{
  "components": {
    "headers": {
      "RateLimit-Limit": {
        "description": "Maximum number of requests allowed in the current window (IETF draft-7).",
        "schema": {
          "example": 1000,
          "type": "integer"
        }
      },
      "RateLimit-Policy": {
        "description": "IETF draft-7 rate limit policy string, `<limit>;w=<window-seconds>`.\n\n**Emitted only when the deployment runs the Redis limiter backend.** The default\nbackend is PostgreSQL, which never emits this header. Treat it as optional.\n",
        "schema": {
          "example": "1000;w=60",
          "type": "string"
        }
      },
      "RateLimit-Remaining": {
        "description": "Requests remaining in the current window (IETF draft-7).",
        "schema": {
          "example": 999,
          "type": "integer"
        }
      },
      "RateLimit-Reset": {
        "description": "**Seconds remaining** until the current window resets (a delta, per IETF draft-7)\n— not an absolute timestamp. Compare with `X-RateLimit-Reset`, which is absolute.\n",
        "schema": {
          "example": 42,
          "type": "integer"
        }
      },
      "X-RateLimit-Limit": {
        "description": "Legacy alias of `RateLimit-Limit` — the maximum number of requests allowed in\nthe current window. Emitted only by rate-limited routes; unlimited routes emit\nno rate limit headers.\n",
        "schema": {
          "example": 1000,
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Legacy alias of `RateLimit-Remaining` — requests remaining in the current window.\n",
        "schema": {
          "example": 999,
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Absolute time at which the current window resets. **The encoding depends on the\nserver's limiter backend and is NOT the same quantity as `RateLimit-Reset`**\n(which is a delta in seconds):\n\n- PostgreSQL backend (the default): **Unix epoch seconds**, e.g. `1704067260`.\n- Redis backend: an **ISO-8601 timestamp string**, e.g. `2026-01-01T00:01:00.000Z`.\n\nClients that need one stable shape should read `RateLimit-Reset` instead.\n",
        "schema": {
          "oneOf": [
            {
              "description": "Unix epoch seconds (PostgreSQL limiter backend)",
              "example": 1704067260,
              "type": "integer"
            },
            {
              "description": "ISO-8601 timestamp (Redis limiter backend)",
              "example": "2026-01-01T00:01:00.000Z",
              "format": "date-time",
              "type": "string"
            }
          ]
        }
      }
    },
    "responses": {
      "BadRequest": {
        "content": {
          "application/json": {
            "example": {
              "message": "Missing required fields",
              "success": false
            },
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Bad request"
      },
      "Error": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error__sdk"
            }
          }
        },
        "description": "Canonical Trillboards error envelope."
      },
      "InternalError": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "properties": {
                    "code": {
                      "example": "internal_error",
                      "type": "string"
                    },
                    "doc_url": {
                      "example": "https://api.trillboards.com/docs/errors#internal_error",
                      "format": "uri",
                      "type": "string"
                    },
                    "message": {
                      "example": "An internal error occurred. Please contact support with request_id req_...",
                      "type": "string"
                    },
                    "request_id": {
                      "pattern": "^req_[a-f0-9]{32}$",
                      "type": "string"
                    },
                    "type": {
                      "enum": [
                        "api_error"
                      ],
                      "type": "string"
                    }
                  },
                  "required": [
                    "type",
                    "code",
                    "message",
                    "request_id"
                  ],
                  "type": "object"
                }
              },
              "required": [
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "Canonical Trillboards error envelope for unexpected server errors.\nSurfaces the request_id so the caller can quote it to support for\nlog correlation.\n"
      },
      "NotFound": {
        "content": {
          "application/json": {
            "example": {
              "message": "Device not found",
              "success": false
            },
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Resource not found"
      },
      "PaymentRequired": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "properties": {
                    "billing_setup_url": {
                      "example": "/v1/partner/billing/setup",
                      "type": "string"
                    },
                    "code": {
                      "enum": [
                        "payment_required"
                      ],
                      "type": "string"
                    },
                    "help": {
                      "example": "POST /v1/partner/billing/setup with a Stripe payment method token to enable pay-per-use billing.",
                      "type": "string"
                    },
                    "message": {
                      "example": "Free tier exhausted for data_api. Add a payment method to continue.",
                      "type": "string"
                    },
                    "pricing": {
                      "description": "Graduated pricing tiers for this product",
                      "nullable": true,
                      "type": "object"
                    },
                    "pricing_url": {
                      "example": "/v1/partner/pricing",
                      "type": "string"
                    },
                    "product": {
                      "description": "Product that exhausted its free tier",
                      "enum": [
                        "data_api",
                        "proof_of_play",
                        "attribution",
                        "data_marketplace"
                      ],
                      "type": "string"
                    },
                    "type": {
                      "enum": [
                        "billing_error"
                      ],
                      "type": "string"
                    },
                    "usage": {
                      "properties": {
                        "current": {
                          "description": "Current usage count this billing period",
                          "example": 12000,
                          "type": "integer"
                        },
                        "exceeded_by": {
                          "description": "How many units over the free limit",
                          "example": 2000,
                          "type": "integer"
                        },
                        "free_limit": {
                          "description": "Free tier limit per month",
                          "example": 10000,
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                },
                "request_id": {
                  "example": "req_abc123",
                  "type": "string"
                }
              },
              "type": "object"
            }
          }
        },
        "description": "Free tier exhausted — add a payment method to continue"
      },
      "TooManyRequests": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "message": {
                  "example": "API rate limit exceeded. Maximum 1000 requests per minute.",
                  "type": "string"
                },
                "retryAfter": {
                  "example": 60,
                  "type": "integer"
                },
                "success": {
                  "example": false,
                  "type": "boolean"
                }
              },
              "type": "object"
            }
          }
        },
        "description": "Rate limit exceeded",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying",
            "schema": {
              "example": 60,
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        }
      },
      "Unauthorized": {
        "content": {
          "application/json": {
            "example": {
              "message": "API key required",
              "success": false
            },
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Unauthorized - Invalid or missing API key"
      }
    },
    "schemas": {
      "Ad": {
        "properties": {
          "allocation_id": {
            "description": "Placement/allocation ID used for delivery accounting, make-goods, and pacing.",
            "type": "string"
          },
          "duration": {
            "description": "Duration in seconds",
            "type": "integer"
          },
          "id": {
            "description": "Advertisement ID",
            "type": "string"
          },
          "impression_id": {
            "description": "Server-generated impression ID that must be echoed when reporting proof-of-play.",
            "type": "string"
          },
          "impression_url": {
            "deprecated": true,
            "description": "**Deprecated alias of `tracking.impression_url`.** These two fields are the\nsame string, byte for byte, produced from one value server-side — they are\nnot two different hooks and firing both records the same call twice. Read\n`tracking.impression_url`; this flat field is retained only for clients that\npredate the `tracking` object and will be removed once they are off it.\n",
            "format": "uri",
            "type": "string"
          },
          "title": {
            "description": "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.",
            "type": "string"
          },
          "tracking": {
            "description": "Structured proof-of-play URLs for clients that prefer named tracking hooks\nover legacy flat fields.\n\n**Fire exactly one of `impression_url` / `complete_url` per play.** They\nresolve to the same endpoint and differ only by the `completed` query\nparameter, which that endpoint does not act on (see `GET /impression`) —\ncalling both records the play twice.\n",
            "properties": {
              "complete_url": {
                "description": "Call when the ad completes playback/display dwell. This is\n`impression_url` plus `&completed=true`; the flag is inert server-side,\nso this is a delivery-accounting call, not proof of play.\n",
                "format": "uri",
                "type": "string"
              },
              "impression_url": {
                "description": "Call when the ad is presented. Identical to the top-level `impression_url`.",
                "format": "uri",
                "type": "string"
              },
              "owned_direct_complete_url": {
                "description": "**Server-signed proof of play. Present only on owner-billable direct\nbookings; absent on self-promo, house/QR, demo and fully-delivered\nplacements.**\n\nWhen present, fire it VERBATIM — including the `sig`, `request_id` and\n`impid` parameters — once the ad has finished playing. Do not rewrite,\nre-sign, re-order, or drop any query parameter: the signature covers the\nvalues and any edit invalidates it. This is the only completion signal on\nthe `data.ads` rail that settles; `complete_url` above is delivery\naccounting and is not billing-grade.\n\nReplays of the same play are collapsed on the SIGNED `request_id`, which\ncarries this serve's `impression_id`. Because it is covered by `sig`, a\nreplay that alters it to claim a fresh dedup key fails verification and\nrecords nothing — one served play can settle at most once.\n\nFiring it in addition to `complete_url` is correct and does not\ndouble-count — they land in different stores.\n",
                "format": "uri",
                "type": "string"
              }
            },
            "type": "object"
          },
          "type": {
            "description": "`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.",
            "enum": [
              "video",
              "image"
            ],
            "type": "string"
          },
          "url": {
            "description": "Media URL (CDN)",
            "format": "uri",
            "type": "string"
          }
        },
        "type": "object"
      },
      "AdSettings": {
        "properties": {
          "ad_interval": {
            "description": "Partner-configured ad rotation cadence in seconds, served from the screen display setting `ad_interval_seconds` when present and legacy display preferences only as fallback.",
            "type": "integer"
          },
          "content_fit": {
            "$ref": "#/components/schemas/ContentFitSettings"
          },
          "image_duration": {
            "description": "Seconds to display image ads; independent from ad rotation cadence.",
            "type": "integer"
          },
          "max_ads_per_hour": {
            "description": "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.",
            "type": "integer"
          },
          "overlay_button_text": {
            "description": "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.",
            "type": "string"
          },
          "sound_enabled": {
            "description": "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.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "AdvertisingIdentity": {
        "description": "Stateful resettable operating-system advertising identity. Send\n`available` only with the current OS ID; send `limited`,\n`consent_denied`, `unavailable`, `unsupported`, or `error` to clear a\npreviously observed identifier. UID2 and hashed email are audience\ntokens, not OS advertising IDs, and do not belong in this envelope.\n",
        "properties": {
          "expires_at": {
            "description": "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.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "description": "Present only when status is `available`.",
            "maxLength": 128,
            "minLength": 8,
            "nullable": true,
            "type": "string"
          },
          "id_type": {
            "description": "Resettable OS identifier type. GAID maps to OpenRTB `aaid`.",
            "enum": [
              "gaid",
              "aaid",
              "afai",
              "rida",
              "tifa",
              "idfa"
            ],
            "nullable": true,
            "type": "string"
          },
          "limit_ad_tracking": {
            "description": "Must be false for `available` and true for `limited`.",
            "nullable": true,
            "type": "boolean"
          },
          "observed_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "source": {
            "description": "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.",
            "enum": [
              "google_play_services",
              "amazon_fire_os",
              "roku_os",
              "samsung_tizen",
              "lg_webos",
              "apple_platform",
              "host_supplied"
            ],
            "type": "string"
          },
          "source_version": {
            "description": "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`.",
            "maxLength": 128,
            "nullable": true,
            "type": "string"
          },
          "status": {
            "description": "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.",
            "enum": [
              "available",
              "limited",
              "consent_denied",
              "unavailable",
              "unsupported",
              "error"
            ],
            "type": "string"
          }
        },
        "required": [
          "status",
          "limit_ad_tracking",
          "source",
          "observed_at"
        ],
        "type": "object"
      },
      "AnalyticsData": {
        "description": "Partner analytics summary (`@trillboards/api-client` `AnalyticsData`).\n",
        "properties": {
          "average_cpm": {
            "description": "**Never emitted** by the analytics endpoint, which returns impression counts and cached revenue only.",
            "type": "number"
          },
          "by_screen": {
            "description": "Optional per-screen breakdown.",
            "items": {
              "properties": {
                "earnings": {
                  "type": "number"
                },
                "impressions": {
                  "type": "number"
                },
                "name": {
                  "type": "string"
                },
                "screen_id": {
                  "type": "string"
                }
              },
              "required": [
                "screen_id",
                "name",
                "impressions",
                "earnings"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "fill_rate": {
            "description": "Fraction of ad opportunities filled, 0..1.",
            "type": "number"
          },
          "period": {
            "description": "The window this summary covers (e.g. day, week, month).",
            "type": "string"
          },
          "screens_active": {
            "description": "**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.",
            "type": "number"
          },
          "screens_total": {
            "description": "**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.",
            "type": "number"
          },
          "total_earnings": {
            "description": "**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.",
            "type": "number"
          },
          "total_impressions": {
            "description": "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`).",
            "type": "number"
          }
        },
        "required": [
          "period",
          "total_impressions",
          "total_earnings",
          "average_cpm",
          "fill_rate",
          "screens_active",
          "screens_total"
        ],
        "type": "object"
      },
      "AudienceAnalyticsResponse": {
        "properties": {
          "data": {
            "description": "`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).",
            "properties": {
              "audience": {
                "properties": {
                  "avg_attention": {
                    "type": "number"
                  },
                  "avg_face_count": {
                    "type": "number"
                  },
                  "screens_with_data": {
                    "type": "integer"
                  }
                },
                "type": "object"
              },
              "overview": {
                "properties": {
                  "active_screens": {
                    "type": "integer"
                  },
                  "avg_impressions_per_screen": {
                    "type": "integer"
                  },
                  "total_impressions": {
                    "type": "integer"
                  },
                  "total_screens": {
                    "type": "integer"
                  }
                },
                "type": "object"
              },
              "period": {
                "type": "string"
              },
              "timestamp": {
                "format": "date-time",
                "type": "string"
              },
              "trends": {
                "properties": {
                  "audience_trend": {
                    "enum": [
                      "increasing",
                      "stable",
                      "decreasing"
                    ],
                    "type": "string"
                  },
                  "impressions_trend": {
                    "enum": [
                      "increasing",
                      "stable",
                      "decreasing"
                    ],
                    "type": "string"
                  }
                },
                "type": "object"
              }
            },
            "type": "object"
          },
          "request_id": {
            "description": "Per-request identifier (`req_` plus 32 hex), also returned on the `X-Request-Id` response header.",
            "type": "string"
          },
          "success": {
            "description": "Always `true` on a 2xx; failures return the error envelope (`success: false` plus an `error` object) at a 4xx/5xx status.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "AudienceAttentionPrediction": {
        "description": "Backend-derived predictive attention signal + buyer-grade attentive-seconds. 1:1 with audience_metrics. Never null by contract; placeholder on row 1.",
        "properties": {
          "attentive_face_count": {
            "description": "attentive_face_count SMALLINT: distinct viewers meeting the >=1s threshold; the buyer-side CPMA denominator. NEVER NULL.",
            "example": 0,
            "format": "int32",
            "maximum": 1000,
            "minimum": 0,
            "type": "integer",
            "x-since": "2026-05-12T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "count"
          },
          "attentive_seconds_observed": {
            "description": "attentive_seconds_observed = SUM(per_face[].gaze_seconds >= 1.0), OpenOOH 2024 4.2. NEVER NULL by DDL, so 0.000 means 'no viewer met threshold' ONLY when per_face_observations[] was present. NUMERIC(6,3).",
            "example": 500,
            "maximum": 999.999,
            "minimum": 0,
            "type": "number",
            "x-since": "2026-05-12T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "attentive_seconds_predicted_15s": {
            "description": "attentive_seconds_predicted_15s: expected next-15s attentive seconds from per-screen history x attention-weight; bootstrap = linear extrapolation. No global tau. NUMERIC(6,3).",
            "example": 500,
            "maximum": 999.999,
            "minimum": 0,
            "type": "number",
            "x-since": "2026-05-12T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_abs_error": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_attention_forecast": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_attention_realized": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_confidence": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_forecast_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_graded_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_horizon_minutes": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "count"
          },
          "next_state_method": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "text"
          },
          "next_state_persist_abs_error": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-06-16T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_sample_size": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-06-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "count"
          },
          "next_state_xscreen_abs_error": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-06-16T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_xscreen_method": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-06-16T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "text"
          },
          "next_state_xscreen_sample_size": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-06-16T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "count"
          },
          "next_state_xsv_abs_error": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-06-16T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
          },
          "next_state_xsv_sample_size": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-06-16T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "count"
          },
          "predicted_attention_next_30s": {
            "description": "predicted_attention_next_30s enum {high,medium,low,none}. 'none' is below 'low'; fires when rolling history converges to zero or on a trend down from 'low'.",
            "enum": [
              "high",
              "medium",
              "low",
              "none"
            ],
            "example": "high",
            "type": "string",
            "x-since": "2026-05-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "enum"
          },
          "predicted_disengagement_imminent": {
            "description": "predicted_disengagement_imminent needs elevated dwell AND a falling trend, so it is unreachable whenever the history vector is empty. A 0% fire rate is a HISTORY-PLUMBING alarm, not a quiet fleet.",
            "example": false,
            "type": "boolean",
            "x-since": "2026-05-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "boolean"
          },
          "prediction_evidence": {
            "example": "sample",
            "maxLength": 280,
            "minLength": 1,
            "type": "string",
            "x-since": "2026-05-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml",
            "x-units": "text"
          }
        },
        "required": [
          "predicted_attention_next_30s",
          "predicted_disengagement_imminent",
          "prediction_evidence",
          "attentive_seconds_observed",
          "attentive_face_count",
          "attentive_seconds_predicted_15s"
        ],
        "type": "object",
        "x-since": "2026-05-11T00:00:00.000Z",
        "x-sor-ref": "docs/schemas/audience-fein/audience_attention_prediction.yaml"
      },
      "AudienceData": {
        "description": "Live/aggregate audience data for a screen\n(`@trillboards/api-client` `AudienceData`).\n",
        "properties": {
          "attention_count": {
            "description": "Count of attentive viewers in the sample.",
            "type": "number"
          },
          "demographics": {
            "additionalProperties": true,
            "description": "Optional demographic breakdown.",
            "type": "object"
          },
          "dwell_time_avg": {
            "description": "Average dwell time for the sample.",
            "type": "number"
          },
          "screen_id": {
            "description": "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.",
            "type": "string"
          },
          "timestamp": {
            "description": "**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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "screen_id",
          "timestamp",
          "attention_count",
          "dwell_time_avg"
        ],
        "type": "object"
      },
      "AudienceExportResponse": {
        "properties": {
          "data": {
            "description": "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`.",
            "items": {
              "properties": {
                "audience": {
                  "properties": {
                    "avg_attention": {
                      "type": "number"
                    },
                    "avg_face_count": {
                      "type": "number"
                    },
                    "demographics": {
                      "type": "object"
                    },
                    "income_profile": {
                      "type": "object"
                    },
                    "lifestyle_segments": {
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                },
                "country": {
                  "type": "string"
                },
                "impressions": {
                  "type": "integer"
                },
                "location": {
                  "type": "string"
                },
                "period": {
                  "properties": {
                    "end": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "start": {
                      "format": "date-time",
                      "type": "string"
                    }
                  },
                  "type": "object"
                },
                "screen_id": {
                  "type": "string"
                },
                "screen_name": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "request_id": {
            "description": "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.",
            "type": "string"
          },
          "success": {
            "description": "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.",
            "type": "boolean"
          },
          "summary": {
            "description": "`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.",
            "properties": {
              "aggregation": {
                "type": "string"
              },
              "period": {
                "type": "object"
              },
              "total_impressions": {
                "type": "integer"
              },
              "total_screens": {
                "type": "integer"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "AudienceLoyaltyFeaturesRow": {
        "description": "Per-device cross-day loyalty feature row emitted by the\n`audience_loyalty_features` dataset. Keyed by a partner-scoped\npseudonymous ID — the same physical device yields a different ID\nfor each partner because the HMAC salt is partner-specific: one salt\nper partner, whichever API key calls, so the ID is the same ref the\npartner's device-level files carry.\n",
        "properties": {
          "_match_disclosure": {
            "description": "How the cross-day device key was formed. `primary_signal_source` is picked by a fixed precedence (`fixed_mac` > `mdns_vendor_model` > `wifi_bssid` > `ble_continuity` > `ble_mac_stable`) and `match_confidence` is a direct relabelling of it (first three `high`, `ble_continuity` `medium`, `ble_mac_stable` `low`) — not a computed probability. `wifi_bssid` marks a device observed over WiFi, whose key is its BSSID.",
            "properties": {
              "match_confidence": {
                "enum": [
                  "high",
                  "medium",
                  "low"
                ],
                "type": "string"
              },
              "primary_signal_source": {
                "enum": [
                  "ble_continuity",
                  "ble_mac_stable",
                  "mdns_vendor_model",
                  "fixed_mac",
                  "wifi_bssid"
                ],
                "type": "string"
              }
            },
            "type": "object"
          },
          "demographic_inference": {
            "description": "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.",
            "properties": {
              "age_bracket": {
                "type": "string"
              },
              "attention_score_avg": {
                "type": "number"
              },
              "audience_attire_archetype": {
                "type": "string"
              },
              "audience_group_composition": {
                "type": "string"
              },
              "audience_intent_stage": {
                "type": "string"
              },
              "gender_distribution": {
                "type": "object"
              }
            },
            "type": "object"
          },
          "device_id_pseudonymous": {
            "description": "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.",
            "example": "tb_8a2f7c1e4d5b6a92",
            "type": "string"
          },
          "distinct_days_in_window": {
            "description": "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.",
            "type": "integer"
          },
          "distinct_venues_in_window": {
            "description": "Distinct SCREENS, not venues — a site with several screens contributes more than one, so this over-counts locations for any multi-screen venue.",
            "type": "integer"
          },
          "first_seen_relative_days": {
            "description": "Days since this device was first observed at any of the partner-scoped screens.",
            "type": "integer"
          },
          "last_seen_relative_days": {
            "description": "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.",
            "type": "integer"
          },
          "manufacturer_class": {
            "description": "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`.",
            "enum": [
              "premium",
              "mainstream",
              "audio_iot",
              "other"
            ],
            "type": "string"
          },
          "manufacturer_label": {
            "description": "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.",
            "example": "Apple",
            "type": "string"
          },
          "raw_identifiers": {
            "description": "Raw cross-day-stable identifiers when available (NULL for randomized phone MACs without Continuity).",
            "properties": {
              "ble_mac": {
                "nullable": true,
                "type": "string"
              },
              "ble_mac_oui": {
                "description": "First 3 bytes of BLE MAC (IEEE OUI vendor lookup).",
                "nullable": true,
                "type": "string"
              },
              "ble_mac_oui_vendor": {
                "nullable": true,
                "type": "string"
              },
              "manufacturer_company_id": {
                "nullable": true,
                "type": "integer"
              },
              "mdns_hostname": {
                "nullable": true,
                "type": "string"
              },
              "wifi_bssid": {
                "nullable": true,
                "type": "string"
              }
            },
            "type": "object"
          },
          "top_day_of_week": {
            "description": "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.",
            "maximum": 6,
            "minimum": 0,
            "type": "integer"
          },
          "top_hour_of_day": {
            "description": "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.",
            "maximum": 23,
            "minimum": 0,
            "type": "integer"
          },
          "venue_archetypes": {
            "description": "List of venue_category labels (Bar, Restaurant, etc.) the device was observed at.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "visit_count_in_window": {
            "description": "Count of raw radio OBSERVATIONS, not of visits — one dwelling device emits many rows per minute.",
            "type": "integer"
          }
        },
        "required": [
          "device_id_pseudonymous",
          "manufacturer_class",
          "first_seen_relative_days",
          "last_seen_relative_days",
          "visit_count_in_window",
          "distinct_days_in_window"
        ],
        "type": "object"
      },
      "AudienceMetricsResponse": {
        "properties": {
          "data": {
            "description": "**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.",
            "properties": {
              "aggregated": {
                "properties": {
                  "avgAttention": {
                    "type": "number"
                  },
                  "avgDwellMs": {
                    "type": "number"
                  },
                  "avgFaceCount": {
                    "type": "number"
                  },
                  "groupComposition": {
                    "type": "object"
                  },
                  "incomeProfile": {
                    "properties": {
                      "high": {
                        "type": "number"
                      },
                      "low": {
                        "type": "number"
                      },
                      "medium": {
                        "type": "number"
                      },
                      "premium": {
                        "type": "number"
                      }
                    },
                    "type": "object"
                  }
                },
                "type": "object"
              },
              "history": {
                "items": {
                  "type": "object"
                },
                "type": "array"
              },
              "live": {
                "properties": {
                  "avgAttention": {
                    "description": "Average attention score (0-1)",
                    "type": "number"
                  },
                  "avgFaceCount": {
                    "description": "Average faces detected",
                    "type": "number"
                  },
                  "incomeLevel": {
                    "enum": [
                      "unknown",
                      "low",
                      "medium",
                      "high",
                      "premium"
                    ],
                    "type": "string"
                  },
                  "maxFaces": {
                    "description": "Maximum faces detected in period",
                    "type": "integer"
                  },
                  "mood": {
                    "type": "string"
                  },
                  "screenEngagement": {
                    "type": "string"
                  }
                },
                "type": "object"
              },
              "period": {
                "type": "string"
              },
              "screen_id": {
                "type": "string"
              },
              "screen_name": {
                "type": "string"
              },
              "timestamp": {
                "format": "date-time",
                "type": "string"
              }
            },
            "type": "object"
          },
          "request_id": {
            "description": "Per-request identifier (`req_` plus 32 hex), also returned on the `X-Request-Id` response header.",
            "type": "string"
          },
          "success": {
            "description": "Always `true` on a 2xx; failures return the error envelope (`success: false` plus an `error` object) at a 4xx/5xx status.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "AudienceSegment": {
        "properties": {
          "created_at": {
            "description": "Creation time of the authoritative PG row; the 201 create response falls back to the server clock only if the insert returned no row.",
            "format": "date-time",
            "type": "string"
          },
          "description": {
            "description": "Free text, trimmed on write. Defaults to the empty string, never `null`, when omitted at create.",
            "type": "string"
          },
          "id": {
            "description": "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`.",
            "type": "string"
          },
          "is_active": {
            "description": "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.",
            "type": "boolean"
          },
          "last_matched_at": {
            "description": "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.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "last_matched_count": {
            "description": "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.",
            "type": "integer"
          },
          "name": {
            "description": "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.",
            "type": "string"
          },
          "object": {
            "description": "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.",
            "example": "audience_segment",
            "type": "string"
          },
          "request_id": {
            "description": "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.",
            "type": "string"
          },
          "rules": {
            "description": "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.",
            "items": {
              "properties": {
                "field": {
                  "type": "string"
                },
                "operator": {
                  "type": "string"
                },
                "value": {}
              },
              "type": "object"
            },
            "type": "array"
          },
          "updated_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "AudienceSocialAttention": {
        "description": "Buyer-grade social amplification signal (5 typed columns). Cloud Vertex Gemini producer. 1 row per audience observation when cascade is observable.",
        "properties": {
          "amplification_evidence": {
            "example": "sample",
            "maxLength": 280,
            "minLength": 1,
            "type": "string",
            "x-since": "2026-05-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_social_attention.yaml",
            "x-units": "text"
          },
          "cascade_depth": {
            "example": 0,
            "format": "int32",
            "maximum": 10,
            "minimum": 0,
            "type": "integer",
            "x-since": "2026-05-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_social_attention.yaml",
            "x-units": "count"
          },
          "data_projection_version": {
            "example": 0,
            "format": "int64",
            "type": "integer",
            "x-since": "2026-09-30T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_social_attention.yaml",
            "x-units": "count"
          },
          "data_projection_xid": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-09-30T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_social_attention.yaml",
            "x-units": "text"
          },
          "primary_attention_anchor_face_index": {
            "description": "All 5 columns NOT NULL except primary_attention_anchor_face_index (only legit NULL: no anchor or solo scene).",
            "example": 0,
            "format": "int32",
            "minimum": 0,
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_social_attention.yaml",
            "x-units": "count"
          },
          "social_amplification_factor": {
            "description": "NUMERIC(4,3).",
            "example": 0.5,
            "maximum": 1,
            "minimum": 0,
            "type": "number",
            "x-since": "2026-05-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_social_attention.yaml",
            "x-units": "ratio"
          },
          "viral_attention_score": {
            "description": "viral_attention_score is the headline number Viant/T-Vision license. Composite of social_amplification_factor × cascade_depth × emotional engagement. NUMERIC(4,3).",
            "example": 0.5,
            "maximum": 1,
            "minimum": 0,
            "type": "number",
            "x-since": "2026-05-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/audience-fein/audience_social_attention.yaml",
            "x-units": "ratio"
          }
        },
        "required": [
          "social_amplification_factor",
          "cascade_depth",
          "viral_attention_score",
          "amplification_evidence",
          "data_projection_version",
          "data_projection_xid"
        ],
        "type": "object",
        "x-since": "2026-05-11T00:00:00.000Z",
        "x-sor-ref": "docs/schemas/audience-fein/audience_social_attention.yaml"
      },
      "BatchResult": {
        "properties": {
          "failed_count": {
            "description": "Number that failed",
            "type": "integer"
          },
          "failed_screens": {
            "description": "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`.",
            "items": {
              "properties": {
                "error": {
                  "type": "string"
                },
                "screen_id": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "object": {
            "description": "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.",
            "example": "batch_result",
            "type": "string"
          },
          "request_id": {
            "description": "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.",
            "type": "string"
          },
          "total_requested": {
            "description": "Number of screens requested to update",
            "type": "integer"
          },
          "updated_count": {
            "description": "Number successfully updated",
            "type": "integer"
          },
          "updated_screens": {
            "description": "IDs of successfully updated screens",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "BillingInfo": {
        "description": "Partner billing snapshot — plan, credits, and month-to-date usage\n(`@trillboards/api-client` `BillingInfo`).\n",
        "properties": {
          "credits_remaining": {
            "description": "**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.",
            "type": "number"
          },
          "credits_used": {
            "description": "**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.",
            "type": "number"
          },
          "plan": {
            "description": "**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`.",
            "type": "string"
          },
          "stripe_connected": {
            "description": "Whether the partner has a connected Stripe account.",
            "type": "boolean"
          },
          "usage_this_month": {
            "additionalProperties": {
              "type": "number"
            },
            "description": "Usage counters for the current billing month, keyed by metric.",
            "type": "object"
          }
        },
        "required": [
          "plan",
          "credits_remaining",
          "credits_used",
          "usage_this_month",
          "stripe_connected"
        ],
        "type": "object"
      },
      "BleScanResultData": {
        "additionalProperties": true,
        "properties": {
          "appleContinuitySubtype": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "deviceType": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "ibeaconMajor": {
            "description": "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`.",
            "nullable": true,
            "type": "integer"
          },
          "ibeaconMinor": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "ibeaconUuid": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "manufacturerCompanyId": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "rawAddress": {
            "description": "The BLE advertiser address exactly as Android reports it, unhashed on the device. It is sent over TLS, and Trillboards stores it as received and as a keyed HMAC-SHA256 hash. 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.",
            "type": "string"
          },
          "rssi": {
            "description": "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.",
            "maximum": 0,
            "minimum": -120,
            "type": "integer"
          },
          "serviceUuids": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array"
          },
          "stableManufacturerPayloadHex": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "txPowerDbm": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          }
        },
        "required": [
          "rawAddress",
          "rssi"
        ],
        "type": "object"
      },
      "ChangelogEntry": {
        "properties": {
          "date": {
            "description": "ISO date string or null for unreleased",
            "example": "2026-04-16",
            "nullable": true,
            "type": "string"
          },
          "description": {
            "description": "Human-readable change description",
            "example": "New data export endpoint",
            "type": "string"
          },
          "surface": {
            "description": "API surface: data, sdk, device, sensing, openrtb, partner, all",
            "example": "data",
            "type": "string"
          },
          "type": {
            "description": "Keepachangelog type: added, changed, deprecated, removed, fixed, security",
            "example": "added",
            "type": "string"
          },
          "version": {
            "description": "\"unreleased\" or a date like \"2026-04-16\"",
            "example": "unreleased",
            "type": "string"
          }
        },
        "required": [
          "version",
          "date",
          "type",
          "surface",
          "description"
        ],
        "type": "object"
      },
      "ChangelogResponse": {
        "properties": {
          "data": {
            "description": "Matching changelog entries",
            "items": {
              "$ref": "#/components/schemas/ChangelogEntry"
            },
            "type": "array"
          },
          "object": {
            "description": "Always \"list\"",
            "enum": [
              "list"
            ],
            "type": "string"
          },
          "total": {
            "description": "Number of entries returned",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "object",
          "data",
          "total"
        ],
        "type": "object"
      },
      "CompletedImpressions": {
        "description": "Completed-play observations from VAST complete and proof-of-play paths. Deduped by issued request/impression key for live revenue and daily earnings.",
        "properties": {
          "actual_cpm": {
            "description": "NUMERIC(18,6).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "actual_gross_revenue": {
            "description": "NUMERIC(18,6).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "ad_id": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "advertiser_domain": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-10-13T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "bid_request_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "billable_key": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "buyer_seat": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-10-13T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "completed_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "content_sha256": {
            "description": "content_sha256 is DECLARED to propagate from vast_requests via request_id but lands on ~1% of rows: PG vast_requests is empty (vastRequestsWriteBuffer defaultDisabled since 2026-06-26), so that lin...",
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-13T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "created_at": {
            "description": "chEventParityCron: exact-key repair; created_at/oldest authorized floor; original completed_at CH key; same writer/DLQ before ACK; no delivery debit. All leaf pending indexes must be valid; failed/...",
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "creative_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "currency": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "demand_partner_id": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "count"
          },
          "device_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "display_area_ratio": {
            "description": "NUMERIC(5,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "display_mode": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "duration_seconds": {
            "description": "NUMERIC(10,2).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "earnings_deal_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-09-22T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "earnings_deal_type": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-09-22T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "earnings_source": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-09-22T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "event_clock_skew_seconds": {
            "example": 1.5,
            "format": "float",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "event_timestamp_clamped": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "boolean"
          },
          "givt_passed": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "boolean"
          },
          "id": {
            "description": "JustBaat dedupes issued request keys; Adipolo/VidVerto/others use rendered impression_id with request fallback. Readers/rollups dedupe overlapping vast_event, proof_of_play and approved partner_sdk.",
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "count"
          },
          "impression_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "impression_multiplier": {
            "description": "NUMERIC(12,6).",
            "example": 1.5,
            "type": "number",
            "x-since": "2026-07-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "ingestion_path": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "metadata": {
            "description": "SDK missing-price is server-pending until signed native pricing; directbillable_ is not authority. Client debit/mirror/pending claims are stripped. Legacy finalization: recorded/native nonnull zero...",
            "example": {},
            "type": "object",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "normalized_event_timestamp": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "play_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "pricing_source": {
            "description": "PG is the canonical completion write; completed_impressions_dist is its CH reporting mirror. pricing_source: 'placement_purchase'=ad_price/goal per play; '*_overdelivered'/'*_zero'=$0; 'config_defa...",
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "quantity_measured_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-07-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "quantity_source": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-07-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "quantity_source_type": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-07-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "count"
          },
          "quantity_vendor": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-07-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "raw_client_timestamp": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
          },
          "request_id": {
            "description": "owned_direct = signed src or verified SDK. VAST impid, request_id and SDK suffix share one nonce/PG row; completion, exact debit and eligible billing commit together. First authorized write owns CH...",
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "screen_id": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "count"
          },
          "screen_mongo_id": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "serving_tenant_id": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-25T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "count"
          },
          "signature": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "source_provider": {
            "description": "ImpressionController drops rows with unknown source_provider (PR #2924); earnings use partner billing policy, not raw counts. Exclude pi_e2e_*, admin-allocation and is_demo_data from revenue.",
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "variant_id": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "count"
          },
          "variant_name": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          },
          "zone_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml",
            "x-units": "text"
          }
        },
        "required": [
          "completed_at",
          "screen_mongo_id",
          "ad_id",
          "source_provider",
          "pricing_source",
          "currency",
          "ingestion_path",
          "metadata",
          "created_at",
          "impression_multiplier"
        ],
        "type": "object",
        "x-sor-ref": "docs/schemas/ad-lifecycle/completed_impressions.yaml"
      },
      "ContentFitSettings": {
        "additionalProperties": false,
        "description": "Per-content-type sizing returned and accepted by the Partner API.\n`fit` preserves the entire creative and may letterbox; `fill` covers\nthe screen and may crop edges. Omitting this object from a write leaves\nthe screen's current behavior unchanged.\n",
        "properties": {
          "advertisements": {
            "description": "Sizing for direct and programmatic advertisements.",
            "enum": [
              "fit",
              "fill"
            ],
            "type": "string"
          },
          "default_streams": {
            "description": "Sizing for default/background streams.",
            "enum": [
              "fit",
              "fill"
            ],
            "type": "string"
          },
          "self_promo": {
            "description": "Sizing for self-promotion and QR-code house ads.",
            "enum": [
              "fit",
              "fill"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "ContentPreferencesInput": {
        "properties": {
          "blocked_advertisers": {
            "description": "Specific advertiser IDs to block",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "blocked_categories": {
            "description": "Ad categories to block (e.g., \"alcohol\", \"gambling\", \"adult\")",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "max_ad_frequency": {
            "description": "Max times same ad can show per hour",
            "type": "integer"
          },
          "min_ad_rating": {
            "description": "Minimum ad content rating",
            "enum": [
              "G",
              "PG",
              "PG-13",
              "R"
            ],
            "type": "string"
          },
          "preferred_categories": {
            "description": "Preferred ad categories (prioritized in auctions)",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "ContentPreferencesResponse": {
        "properties": {
          "blocked_advertisers": {
            "description": "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`.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "blocked_categories": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "max_ad_frequency": {
            "description": "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.",
            "type": "integer"
          },
          "min_ad_rating": {
            "description": "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.",
            "type": "string"
          },
          "object": {
            "description": "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.",
            "example": "content_preferences",
            "type": "string"
          },
          "preferred_categories": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "request_id": {
            "description": "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.",
            "type": "string"
          },
          "screen_id": {
            "description": "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.",
            "type": "string"
          },
          "updated_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ContentPreset": {
        "properties": {
          "blocked_advertisers": {
            "description": "Advertiser IDs blocked",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "blocked_categories": {
            "description": "Categories blocked by this preset",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "description": {
            "description": "Preset description",
            "type": "string"
          },
          "id": {
            "description": "Unique preset identifier",
            "type": "string"
          },
          "min_ad_rating": {
            "description": "Minimum ad rating allowed",
            "enum": [
              "G",
              "PG",
              "PG-13",
              "R"
            ],
            "type": "string"
          },
          "name": {
            "description": "Human-readable preset name",
            "type": "string"
          },
          "preferences": {
            "description": "Full preference object applied by this preset",
            "type": "object"
          }
        },
        "type": "object"
      },
      "CreatePartnerSensingRouteRequest": {
        "properties": {
          "actions": {
            "description": "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.",
            "items": {
              "$ref": "#/components/schemas/SensingRouteAction"
            },
            "minItems": 1,
            "type": "array"
          },
          "activeDaysMask": {
            "default": 127,
            "description": "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`.",
            "maximum": 127,
            "minimum": 0,
            "type": "integer"
          },
          "activeEndMinute": {
            "description": "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.",
            "maximum": 1439,
            "minimum": 0,
            "type": "integer"
          },
          "activeStartMinute": {
            "description": "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.",
            "maximum": 1439,
            "minimum": 0,
            "type": "integer"
          },
          "activeTz": {
            "description": "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.",
            "type": "string"
          },
          "condition": {
            "$ref": "#/components/schemas/SensingRouteCondition"
          },
          "debounceSeconds": {
            "default": 300,
            "description": "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.",
            "minimum": 0,
            "type": "integer"
          },
          "digest": {
            "description": "Required when mode=digest. `field` is required for avg/max.",
            "properties": {
              "field": {
                "type": "string"
              },
              "intervalMinutes": {
                "minimum": 5,
                "type": "integer"
              },
              "metric": {
                "enum": [
                  "count",
                  "avg",
                  "max"
                ],
                "type": "string"
              },
              "windowMinutes": {
                "minimum": 1,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "enabled": {
            "default": true,
            "description": "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.",
            "type": "boolean"
          },
          "mode": {
            "default": "threshold",
            "description": "`threshold` (the default) needs a `condition` and evaluates every incoming observation. `digest` needs a `digest` block instead, sends on a schedule, and rejects `screenFocusRegions` and explicit review timing. Only the exact string `digest` selects digest mode; anything else falls back to `threshold`.",
            "enum": [
              "threshold",
              "digest"
            ],
            "type": "string"
          },
          "name": {
            "description": "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.",
            "maxLength": 200,
            "type": "string"
          },
          "profileName": {
            "description": "Bind to a named Sense Anything profile (stable across redeploys).",
            "type": "string"
          },
          "reviewTimingMode": {
            "$ref": "#/components/schemas/SensingRouteReviewTimingMode"
          },
          "reviewWaitSeconds": {
            "$ref": "#/components/schemas/SensingRouteReviewWaitSeconds"
          },
          "screenFocusRegions": {
            "description": "Optional per-camera rectangles or polygon lassos. Every entry must reference screenMongoIds. Supported only for visual threshold alerts using once_per_episode, on_escalation, or nag_until_resolved cadence; not digest, group-targeted, non-visual, or per_new_entity routes.",
            "items": {
              "$ref": "#/components/schemas/SensingRouteScreenFocusRegion"
            },
            "maxItems": 250,
            "type": "array"
          },
          "screenMongoIds": {
            "description": "Screens to watch — all owned by this partner (foreign → 403). Provide EITHER screenMongoIds OR targetGroupId, not both.",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "type": "array"
          },
          "sustainMinSamples": {
            "description": "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.",
            "minimum": 1,
            "type": "integer"
          },
          "sustainSeconds": {
            "description": "How long the condition must hold before the FIRST alert, in seconds (600 = ten minutes). Omit to alert on the first true reading. Cadence-independent, because the evaluator compares timestamps - correct however often the screen reports, including when that rate changes while the condition holds.",
            "maximum": 86400,
            "minimum": 1,
            "type": "integer"
          },
          "sustainWindowSeconds": {
            "description": "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.",
            "minimum": 0,
            "type": "integer"
          },
          "targetGroupId": {
            "description": "A sg_ Screen Group id (owned by this partner) to target instead of an explicit screen set. The route fires on the group's live members. Requires SENSING_ROUTES_GROUP_TARGET_ENABLED.",
            "type": "string"
          }
        },
        "required": [
          "name",
          "actions"
        ],
        "type": "object"
      },
      "CreativeClassification": {
        "description": "Structured creative classification — the same shape the Trillboards venue portal renders for a served ad.",
        "properties": {
          "advertiser_name": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "age_rating": {
            "description": "One of G, PG, PG-13, R, NC-17 (null until classified).",
            "nullable": true,
            "type": "string"
          },
          "analysis": {
            "description": "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`.",
            "properties": {
              "keywords": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "mood": {
                "nullable": true,
                "type": "string"
              },
              "quality": {
                "nullable": true,
                "type": "string"
              },
              "setting": {
                "nullable": true,
                "type": "string"
              },
              "subjects": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "text_content": {
                "description": "On-screen text (OCR).",
                "nullable": true,
                "type": "string"
              }
            },
            "type": "object"
          },
          "categorization": {
            "description": "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'.",
            "properties": {
              "brand": {
                "nullable": true,
                "type": "string"
              },
              "category": {
                "nullable": true,
                "type": "string"
              },
              "iab_categories": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "type": "object"
          },
          "confidence": {
            "description": "Classification confidence, 0..1.",
            "nullable": true,
            "type": "number"
          },
          "content_sha256": {
            "description": "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`.",
            "example": "6c4b8f3e2a1d…e1",
            "type": "string"
          },
          "id": {
            "description": "Canonical reference — `sha256:<hex>` of the creative bytes. Use as `creative_ref` when posting plays.",
            "example": "sha256:6c4b8f3e2a1d…e1",
            "type": "string"
          },
          "moderation": {
            "description": "`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.",
            "properties": {
              "approved": {
                "type": "boolean"
              },
              "flags": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "type": "object"
          },
          "object": {
            "description": "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.",
            "example": "creative_classification",
            "type": "string"
          },
          "status": {
            "description": "`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.",
            "enum": [
              "pending",
              "approved",
              "flagged",
              "blocked"
            ],
            "type": "string"
          },
          "thumbnail_url": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          }
        },
        "type": "object"
      },
      "CreativeRewardsReadyEvent": {
        "description": "Native readiness hint delivered through existing partner SSE and the audited webhook queue. The hint is free; only the canonical reward page uses native paid admission.",
        "properties": {
          "data": {
            "properties": {
              "advertisement_id": {
                "type": "string"
              },
              "build_stamp": {
                "description": "Lossless native build stamp.",
                "type": "string"
              },
              "builder_version": {
                "type": "string"
              },
              "experiment_id": {
                "type": "string"
              },
              "from": {
                "format": "date",
                "type": "string"
              },
              "notification_id": {
                "pattern": "^cfr_[a-f0-9]{64}$",
                "type": "string"
              },
              "object": {
                "enum": [
                  "creative_reward_ready"
                ],
                "type": "string"
              },
              "recovery": {
                "enum": [
                  "rewind_and_dedupe"
                ],
                "type": "string"
              },
              "request_idempotency_key_required": {
                "enum": [
                  true
                ],
                "type": "boolean"
              },
              "reward_reader_url": {
                "description": "Canonical native reward path plus date scope; never contains credentials or a paid request key.",
                "type": "string"
              },
              "to": {
                "format": "date",
                "type": "string"
              },
              "view": {
                "enum": [
                  "latest_state"
                ],
                "type": "string"
              }
            },
            "required": [
              "object",
              "notification_id",
              "experiment_id",
              "advertisement_id",
              "from",
              "to",
              "build_stamp",
              "builder_version",
              "reward_reader_url",
              "view",
              "recovery",
              "request_idempotency_key_required"
            ],
            "type": "object"
          },
          "event": {
            "enum": [
              "creative.rewards_ready"
            ],
            "type": "string"
          },
          "id": {
            "description": "Audited webhook notification ID; SSE data carries the same ID inside data.notification_id.",
            "type": "string"
          },
          "timestamp": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "event",
          "timestamp",
          "data"
        ],
        "type": "object"
      },
      "CsiApi.HistoryBucket": {
        "properties": {
          "avgMotion": {
            "description": "Mean motion_score across samples in the bucket.",
            "example": 0.37,
            "minimum": 0,
            "type": "number"
          },
          "avgOccupancy": {
            "description": "Mean occupant_count across samples in the bucket.",
            "example": 2.4,
            "minimum": 0,
            "type": "number"
          },
          "peakOccupancy": {
            "description": "Maximum occupant_count observed in the bucket.",
            "example": 6,
            "minimum": 0,
            "type": "integer"
          },
          "sampleCount": {
            "description": "Number of CSI windows aggregated into the bucket.",
            "example": 5,
            "minimum": 0,
            "type": "integer"
          },
          "timestamp": {
            "description": "ISO-8601 UTC start of the 10-minute occupancy bucket.",
            "example": "2026-07-22T18:30:00.000Z",
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "timestamp",
          "avgOccupancy",
          "peakOccupancy",
          "avgMotion",
          "sampleCount"
        ],
        "type": "object"
      },
      "CsiApi.HistoryResponse": {
        "properties": {
          "buckets": {
            "description": "Descending-by-time list of 10-minute occupancy buckets.",
            "items": {
              "$ref": "#/components/schemas/CsiApi.HistoryBucket"
            },
            "type": "array"
          },
          "hours": {
            "description": "Resolved look-back window in hours (after clamping).",
            "example": 24,
            "maximum": 168,
            "minimum": 1,
            "type": "integer"
          },
          "screenId": {
            "example": "65a1b2c3d4e5f60718293a4b",
            "type": "string"
          }
        },
        "required": [
          "screenId",
          "hours",
          "buckets"
        ],
        "type": "object"
      },
      "CsiApi.OccupancyActive": {
        "properties": {
          "dataAge": {
            "description": "Milliseconds elapsed between windowEnd and the response time.",
            "example": 4200,
            "minimum": 0,
            "type": "integer"
          },
          "hardwareType": {
            "description": "Sensing hardware model that produced the window.",
            "example": "esp32-s3",
            "type": "string"
          },
          "motionScore": {
            "description": "Normalised motion energy in [0,1] over the window.",
            "example": 0.42,
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "occupantCount": {
            "description": "Estimated number of occupants inferred for the sensing window.",
            "example": 3,
            "minimum": 0,
            "type": "integer"
          },
          "screenId": {
            "example": "65a1b2c3d4e5f60718293a4b",
            "type": "string"
          },
          "signalQuality": {
            "description": "CSI signal quality in [0,1]; lower values indicate noisier RF.",
            "example": 0.87,
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "status": {
            "enum": [
              "active"
            ],
            "type": "string"
          },
          "windowEnd": {
            "description": "ISO-8601 UTC end of the CSI aggregation window.",
            "example": "2026-07-22T18:31:00.000Z",
            "format": "date-time",
            "type": "string"
          },
          "windowStart": {
            "description": "ISO-8601 UTC start of the CSI aggregation window.",
            "example": "2026-07-22T18:30:00.000Z",
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "screenId",
          "status",
          "occupantCount",
          "motionScore",
          "signalQuality",
          "hardwareType",
          "windowStart",
          "windowEnd",
          "dataAge"
        ],
        "type": "object"
      },
      "CsiApi.OccupancyNoData": {
        "properties": {
          "message": {
            "example": "No CSI sensing data available. Ensure an ESP32-S3 node is active.",
            "type": "string"
          },
          "screenId": {
            "example": "65a1b2c3d4e5f60718293a4b",
            "type": "string"
          },
          "status": {
            "enum": [
              "no_data"
            ],
            "type": "string"
          }
        },
        "required": [
          "screenId",
          "status",
          "message"
        ],
        "type": "object"
      },
      "CsiApi.OccupancyResponse": {
        "discriminator": {
          "mapping": {
            "active": "#/components/schemas/CsiApi.OccupancyActive",
            "no_data": "#/components/schemas/CsiApi.OccupancyNoData"
          },
          "propertyName": "status"
        },
        "oneOf": [
          {
            "$ref": "#/components/schemas/CsiApi.OccupancyActive"
          },
          {
            "$ref": "#/components/schemas/CsiApi.OccupancyNoData"
          }
        ]
      },
      "DashboardResponse": {
        "properties": {
          "earnings": {
            "description": "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.",
            "properties": {
              "available_payout_cents": {
                "type": "integer"
              },
              "next_payout_date": {
                "format": "date",
                "type": "string"
              },
              "pending_payout_cents": {
                "type": "integer"
              },
              "revenue_share_percent": {
                "type": "number"
              },
              "total_earned_cents": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "partner": {
            "description": "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.",
            "properties": {
              "created_at": {
                "format": "date-time",
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "status": {
                "enum": [
                  "pending",
                  "active",
                  "suspended",
                  "terminated"
                ],
                "type": "string"
              }
            },
            "type": "object"
          },
          "recent_activity": {
            "description": "The ten newest `impression_ledger` rows for the partner, newest first. `type` is always the literal `impression` — no other activity kind is ever emitted.",
            "items": {
              "properties": {
                "ad_id": {
                  "type": "string"
                },
                "screen_id": {
                  "type": "string"
                },
                "timestamp": {
                  "format": "date-time",
                  "type": "string"
                },
                "type": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "stats": {
            "description": "`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.",
            "properties": {
              "active_screens": {
                "type": "integer"
              },
              "offline_screens": {
                "type": "integer"
              },
              "pending_screens": {
                "type": "integer"
              },
              "total_impressions_month": {
                "type": "integer"
              },
              "total_impressions_today": {
                "type": "integer"
              },
              "total_impressions_week": {
                "type": "integer"
              },
              "total_screens": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "stripe_status": {
            "description": "`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.",
            "properties": {
              "account_status": {
                "type": "string"
              },
              "connect_account_id": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "top_screens": {
            "description": "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.",
            "items": {
              "properties": {
                "impressions_today": {
                  "type": "integer"
                },
                "location": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "screen_id": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "DataExportSummaryResponse": {
        "description": "Per-dataset record counts + date ranges for the partner's screens.\nReturned by `GET /data-export/summary`.\n",
        "properties": {
          "data": {
            "description": "`lookback_days` is `min(?days, 400)` and defaults to 7. `screen_count` is how many screens the export is SCOPED to, not how many hold data. 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.",
            "properties": {
              "datasets": {
                "additionalProperties": {
                  "properties": {
                    "earliest": {
                      "format": "date-time",
                      "nullable": true,
                      "type": "string"
                    },
                    "latest": {
                      "format": "date-time",
                      "nullable": true,
                      "type": "string"
                    },
                    "total_records": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                },
                "description": "Map of dataset name to record summary",
                "type": "object"
              },
              "lookback_days": {
                "type": "integer"
              },
              "screen_count": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "success": {
            "description": "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.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "DemandPartners": {
        "description": "Canonical INT-PK identity table for demand-side partners (DSPs/SSPs that Trillboards routes ad requests to). Wave 2 of pristine-state refactor. Replaces partner_key TEXT joins across the billing path.",
        "properties": {
          "archived": {
            "example": false,
            "type": "boolean",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml",
            "x-units": "boolean"
          },
          "created_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml"
          },
          "demand_partner_id": {
            "description": "demand_partner_id INT is the canonical FK going forward; partner_key TEXT is kept as the wire-format alternate-key FOREVER (external partner-API auth + admin UI rely on it). Never propose dropping ...",
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml",
            "x-units": "text"
          },
          "display_name": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml",
            "x-units": "text"
          },
          "partner_key": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml",
            "x-units": "text"
          },
          "partner_type": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml",
            "x-units": "text"
          },
          "public_alias": {
            "description": "public_alias UNIQUE: this is the obfuscated external alias partner-API consumers see. partnerAlias.js will read from this table after Wave 2 cutover.",
            "example": "sample",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml",
            "x-units": "text"
          },
          "updated_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml"
          }
        },
        "required": [
          "partner_key",
          "public_alias",
          "display_name",
          "partner_type",
          "archived",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "x-since": "2026-05-01T00:00:00.000Z",
        "x-sor-ref": "docs/schemas/partnerships/demand_partners.yaml"
      },
      "DemographicsResponse": {
        "properties": {
          "data": {
            "description": "`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.",
            "properties": {
              "data_quality": {
                "properties": {
                  "confidence": {
                    "type": "number"
                  },
                  "sample_size": {
                    "type": "integer"
                  }
                },
                "type": "object"
              },
              "demographics": {
                "properties": {
                  "age_distribution": {
                    "properties": {
                      "18-25": {
                        "type": "number"
                      },
                      "26-35": {
                        "type": "number"
                      },
                      "36-50": {
                        "type": "number"
                      },
                      "50+": {
                        "type": "number"
                      }
                    },
                    "type": "object"
                  },
                  "gender_split": {
                    "properties": {
                      "female": {
                        "type": "number"
                      },
                      "male": {
                        "type": "number"
                      }
                    },
                    "type": "object"
                  },
                  "group_composition": {
                    "properties": {
                      "couples": {
                        "type": "number"
                      },
                      "families": {
                        "type": "number"
                      },
                      "friends": {
                        "type": "number"
                      },
                      "solo": {
                        "type": "number"
                      },
                      "work_groups": {
                        "type": "number"
                      }
                    },
                    "type": "object"
                  },
                  "income_profile": {
                    "properties": {
                      "high": {
                        "type": "number"
                      },
                      "low": {
                        "type": "number"
                      },
                      "medium": {
                        "type": "number"
                      },
                      "premium": {
                        "type": "number"
                      }
                    },
                    "type": "object"
                  },
                  "lifestyle_segments": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              },
              "period": {
                "type": "string"
              },
              "screen_id": {
                "type": "string"
              },
              "screen_name": {
                "type": "string"
              },
              "timestamp": {
                "format": "date-time",
                "type": "string"
              }
            },
            "type": "object"
          },
          "request_id": {
            "description": "Per-request identifier (`req_` plus 32 hex), also returned on the `X-Request-Id` response header.",
            "type": "string"
          },
          "success": {
            "description": "Always `true` on a 2xx; failures return the error envelope (`success: false` plus an `error` object) at a 4xx/5xx status.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "Device": {
        "properties": {
          "created_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "device_id": {
            "description": "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.",
            "type": "string"
          },
          "device_type": {
            "description": "Saved partner category, independent of advertising classification. POST with an existing device_id and PATCH accept category updates. Legacy devices without a saved declaration retain their previous numeric-derived label until explicitly updated.",
            "type": "string"
          },
          "display": {
            "description": "Screen display specification. `width`/`height` are pixel resolution;\n`width_in`/`height_in` are physical inches. Pixel-only inputs are\nNOT recorded as physical size — pass `width_in`/`height_in` (most\naccurate) or `ppi` so the server can derive inches from pixels.\n",
            "properties": {
              "height": {
                "description": "Display resolution height in pixels",
                "type": "integer"
              },
              "height_in": {
                "description": "Physical screen height in inches",
                "type": "number"
              },
              "orientation": {
                "type": "string"
              },
              "ppi": {
                "description": "Pixels per inch (required to derive physical size from width/height)",
                "type": "number"
              },
              "width": {
                "description": "Display resolution width in pixels",
                "type": "integer"
              },
              "width_in": {
                "description": "Physical screen width in inches",
                "type": "number"
              }
            },
            "type": "object"
          },
          "embed_url": {
            "description": "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.",
            "type": "string"
          },
          "enrichment": {
            "description": "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.",
            "type": "object"
          },
          "external_device_id": {
            "description": "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.",
            "type": "string"
          },
          "fingerprint": {
            "description": "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`.",
            "type": "string"
          },
          "impression_multiplier": {
            "description": "Saved publisher-declared audience impressions per ad play; defaults to 1. Update via PATCH /v1/partner/device/{deviceId}/impression-multiplier.",
            "example": 2.4,
            "maximum": 100,
            "minimum": 0.1,
            "type": "number"
          },
          "location": {
            "description": "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.",
            "type": "object"
          },
          "location_capability": {
            "$ref": "#/components/schemas/PartnerLocationCapability"
          },
          "monetization_status": {
            "description": "Stripe-style readiness, the same contract registration returns (see https://api.trillboards.com/docs/monetization-requirements).",
            "enum": [
              "ready",
              "limited",
              "blocked"
            ],
            "type": "string"
          },
          "name": {
            "description": "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.",
            "type": "string"
          },
          "requirements": {
            "$ref": "#/components/schemas/PartnerMonetizationRequirements"
          },
          "screen_id": {
            "description": "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.",
            "type": "string"
          },
          "status": {
            "description": "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.",
            "enum": [
              "pending",
              "approved",
              "active",
              "offline",
              "suspended"
            ],
            "type": "string"
          },
          "updated_at": {
            "description": "`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.",
            "format": "date-time",
            "type": "string"
          },
          "venue": {
            "description": "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`.",
            "type": "object"
          }
        },
        "type": "object"
      },
      "DeviceApi.DeviceAuthRejection": {
        "additionalProperties": false,
        "properties": {
          "code": {
            "description": "HTTP status, repeated in the body.",
            "example": 401,
            "type": "integer"
          },
          "error": {
            "description": "Machine-readable reason code.",
            "example": "invalid_device_token",
            "type": "string"
          },
          "message": {
            "example": "Invalid or expired device token",
            "type": "string"
          },
          "status": {
            "enum": [
              false
            ],
            "type": "boolean"
          }
        },
        "required": [
          "status",
          "code",
          "message",
          "error"
        ],
        "type": "object"
      },
      "DeviceApi.OtaManifestEntry": {
        "properties": {
          "agentType": {
            "type": "string"
          },
          "downloadUrl": {
            "format": "uri",
            "type": "string"
          },
          "releaseChannel": {
            "type": "string"
          },
          "releasedAt": {
            "format": "date-time",
            "type": "string"
          },
          "sha256": {
            "type": "string"
          },
          "sizeBytes": {
            "minimum": 0,
            "type": "integer"
          },
          "versionCode": {
            "type": "integer"
          },
          "versionName": {
            "type": "string"
          }
        },
        "required": [
          "agentType",
          "versionName",
          "versionCode",
          "downloadUrl",
          "sha256",
          "sizeBytes"
        ],
        "type": "object"
      },
      "DeviceApi.OtaManifestResponse": {
        "properties": {
          "algorithm": {
            "type": "string"
          },
          "code": {
            "type": "integer"
          },
          "hashAlgorithm": {
            "type": "string"
          },
          "keyId": {
            "type": "string"
          },
          "manifest": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/DeviceApi.OtaManifestEntry"
                },
                "type": "array"
              },
              {
                "additionalProperties": {},
                "type": "object"
              }
            ]
          },
          "payloadHash": {
            "type": "string"
          },
          "publishAt": {
            "format": "date-time",
            "type": "string"
          },
          "signature": {
            "type": "string"
          },
          "status": {
            "type": "boolean"
          }
        },
        "required": [
          "status",
          "code",
          "manifest"
        ],
        "type": "object"
      },
      "DeviceApi.SensingProfileAuthError": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/DeviceApi.DeviceAuthRejection"
          },
          {
            "$ref": "#/components/schemas/DeviceApi.SensingProfileMissingBinding"
          }
        ]
      },
      "DeviceApi.SensingProfileField": {
        "additionalProperties": false,
        "properties": {
          "description": {
            "description": "Instruction text handed to the model. MAY be the empty string: most live metrics_schema entries predate the `description` key and the handler maps a missing one to ''. For an alert-focus field this is composed constraint-first and capped at 120 chars, because every fielded connector slices it there.",
            "example": "How many people are waiting in line.",
            "type": "string"
          },
          "enum_options": {
            "description": "Closed value set the model must choose from. Back-filled from the server's own canonical scene enums when the profile entry declared a bare category, so the device is constrained to exactly what the strict preAnalyzed validation accepts.",
            "example": [
              "rushing",
              "normal",
              "leisurely"
            ],
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "example_outputs": {
            "description": "Up to 4 sample readings, shown to the model as few-shot guidance.",
            "example": [
              "stocked",
              "low",
              "empty"
            ],
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                }
              ]
            },
            "type": "array"
          },
          "field": {
            "description": "snake_case field key the on-device model must emit. Either an operator-declared metrics_schema field, or a synthetic `alert_focus_<16 letters>` key naming one saved alert rectangle (services/fein/alertFocusFields.js#alertFocusFieldKey).",
            "example": "queue_length",
            "minLength": 1,
            "type": "string"
          },
          "source": {
            "description": "Present only on alert-focus entries, which stamp it; entries mapped from metrics_schema omit it. Always `vision` when present — speech and fusion fields are filtered out server-side because neither is observable from a single frame.",
            "enum": [
              "vision"
            ],
            "example": "vision",
            "type": "string"
          },
          "type": {
            "description": "Declared type of the field, from the profile's metrics_schema. `hidden` reaches the wire only via an alert-focus entry — the metrics_schema mapper drops hidden fields.",
            "enum": [
              "counter",
              "gauge",
              "category",
              "string",
              "boolean",
              "list",
              "distribution",
              "enum",
              "hidden"
            ],
            "example": "counter",
            "type": "string"
          }
        },
        "required": [
          "field",
          "type",
          "description"
        ],
        "type": "object"
      },
      "DeviceApi.SensingProfileFieldRouting": {
        "additionalProperties": false,
        "description": "WHO ANSWERS EACH FIELD in `fields`, partitioned server-side from `sensing_model_registry`. `vlm ∪ producer ∪ withheld === fields`. `fields` is retained beside it because connectors fielded before this key read it directly, and because `absenceWatchedFields` is defined as a subset of it; a connector that understands `fieldRouting` must use `fieldRouting.vlm` for its prompt and ignore `fields`.",
        "properties": {
          "alertFocusRegions": {
            "description": "The operator's saved region per SYNTHETIC alert-focus field, in normalized [0,1] frame coordinates. BESIDE the producer half and never inside it: that half carries no prompt material and no second home for a fact `sensing_route_screens` already owns, and a region is the ROUTE's fact — the rule rides the model, the geometry rides the route. Both a rect and a lasso appear here because the containment rule collapses them, and the RECTANGLE form predates the lasso and leaks just as badly. EMPTY on every screen today: all 75 rig `sensing_route_screens` rows carry no region. A connector that ignores this keeps asking the language model, which is what every connector does now — shipping it is additive, and the model has been MEASURED to ignore the English form 100% of the time at every anchor while the geometric rule leaks 0% on the same frames and the same bands.",
            "example": [],
            "items": {
              "properties": {
                "field": {
                  "minLength": 1,
                  "type": "string"
                },
                "region": {
                  "properties": {
                    "height": {
                      "type": "number"
                    },
                    "points": {
                      "items": {
                        "properties": {
                          "x": {
                            "type": "number"
                          },
                          "y": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "x",
                          "y"
                        ],
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "width": {
                      "type": "number"
                    },
                    "x": {
                      "type": "number"
                    },
                    "y": {
                      "type": "number"
                    }
                  },
                  "type": "object"
                }
              },
              "required": [
                "field",
                "region"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "operatingPoints": {
            "additionalProperties": {
              "type": "number"
            },
            "description": "The confidence each producer CLASS is counted at, in the WIRE spelling `/detect` resolves (`fire extinguisher`, not `fire_extinguisher`), as that model's own score card fitted it. Keyed by CLASS and not by field because `person_present` and `person_count` are two fields with one operating point. A class absent here has no committed measurement and keeps the connector's DEFAULT_PRODUCER_MIN_CONFIDENCE; a class two models fitted differently is withheld by the server, for the same reason a colliding field is, and is absent rather than resolved. Without this every class fell back to 0.25, which against the live registry is wrong in BOTH directions — `fall detected` fits 0.8617 and `smoke` 0.05.",
            "example": {
              "forklift": 0.7434567809104919,
              "person": 0.29385938104622616
            },
            "type": "object"
          },
          "producer": {
            "description": "Fields a granted specialist model declares as its own output. These are removed from the prompt so the detector answer is not silently discarded in favour of a language model guess at the same quantity.",
            "items": {
              "$ref": "#/components/schemas/DeviceApi.SensingProfileProducerField"
            },
            "type": "array"
          },
          "vlm": {
            "description": "The ONLY half the on-device language model is asked for. The connector hands exactly this to its prompt and schema builders, so a producer-owned field cannot reach the prompt — it is not in the array they were given, rather than being filtered out of one that was.",
            "items": {
              "$ref": "#/components/schemas/DeviceApi.SensingProfileField"
            },
            "type": "array"
          },
          "withheld": {
            "description": "Fields that reach neither half, each with its reason. EMPTY on a healthy registry — a non-empty array is a publishable defect the gate should have refused, surfaced rather than resolved by guessing.",
            "example": [],
            "items": {
              "$ref": "#/components/schemas/DeviceApi.SensingProfileWithheldField"
            },
            "type": "array"
          }
        },
        "required": [
          "vlm",
          "producer",
          "withheld",
          "operatingPoints",
          "alertFocusRegions"
        ],
        "type": "object"
      },
      "DeviceApi.SensingProfileMissingBinding": {
        "additionalProperties": false,
        "properties": {
          "code": {
            "enum": [
              "DEVICE_AUTH_REQUIRED"
            ],
            "type": "string"
          },
          "error": {
            "example": "A valid device token is required.",
            "type": "string"
          },
          "success": {
            "enum": [
              false
            ],
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "code",
          "error"
        ],
        "type": "object"
      },
      "DeviceApi.SensingProfileModel": {
        "additionalProperties": false,
        "properties": {
          "modelFormat": {
            "description": "The artifact format the device would be handed (`onnx`, `tflite`, `litert-lm`, `mediapipe`, `mlkit`). The connector loads only the formats it has a destination for and IGNORES the rest — an unknown format is skipped with its reason, never guessed at. Null when neither the platform variant nor the registry row declares one.",
            "example": "onnx",
            "nullable": true,
            "type": "string"
          },
          "modelId": {
            "description": "`sensing_model_registry.model_id`. This is also the `modelType` the connector sends to GET /v1/federated/models — `resolveGlobalOtaRegistryId` accepts a registry id directly, so no alias has to be minted for a new model.",
            "example": "rfdetr_medium",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "modelId",
          "modelFormat"
        ],
        "type": "object"
      },
      "DeviceApi.SensingProfileProducerField": {
        "additionalProperties": false,
        "properties": {
          "field": {
            "description": "snake_case field key a granted producer model declares as an output.",
            "example": "queue_length",
            "minLength": 1,
            "type": "string"
          },
          "modelId": {
            "description": "`sensing_model_registry.model_id` of the model that declares this field in its `outputs_schema` — the one producer, derived from the registry by `services/fein/modelClassLiveness.js#fieldProducers`. Never two: the publish gate refuses a registry in which two active models claim one field.",
            "example": "efficientdet",
            "minLength": 1,
            "type": "string"
          },
          "type": {
            "description": "Declared type of the field, from the profile's metrics_schema. `hidden` reaches the wire only via an alert-focus entry — the metrics_schema mapper drops hidden fields.",
            "enum": [
              "counter",
              "gauge",
              "category",
              "string",
              "boolean",
              "list",
              "distribution",
              "enum",
              "hidden"
            ],
            "example": "counter",
            "type": "string"
          }
        },
        "required": [
          "field",
          "type",
          "modelId"
        ],
        "type": "object"
      },
      "DeviceApi.SensingProfileResponse": {
        "additionalProperties": false,
        "properties": {
          "absenceWatchedFields": {
            "description": "Sorted, de-duplicated subset of `fields[].field` whose ABSENT reading an enabled route on this screen actually fires on (e.g. `shelf_state` for a saved `shelf_state eq 'empty'` alert). Derived server-side from the operator's own route conditions via the evaluator's own comparator, never guessed from a field's spelling. The device lets those — and only those — count emptiness as signal, so an empty-shelf alert still carries its evidence frame. Empty on every screen with no absence-targeting route, which is the default.",
            "example": [
              "shelf_state"
            ],
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "captureIntervalMs": {
            "description": "The profile's declared capture cadence in milliseconds, or null when no profile is deployed. Deliberately UNBOUNDED below: the column is a plain integer and negative values exist in production, so the server does not promise a non-negative number here.",
            "example": 10000,
            "nullable": true,
            "type": "integer"
          },
          "classes": {
            "description": "The class vocabulary the granted models advertise, flat and sorted, DERIVED by `sensingModelRegistry.advertisedClasses` rather than read off `coco_classes`. A model that declares no output fields advertises no classes — so an artifact can be delivered and loaded while none of its classes is yet offered, and the classes light up in the same commit as the producer that writes their answers down. Consumed when the detector cascade is ARMED; shadow mode deliberately sends no class filter.",
            "example": [
              "person"
            ],
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "detectorCascade": {
            "additionalProperties": false,
            "description": "Whether the ARMED detector cascade may suppress this screen's VLM calls, decided server-side from the operator's own enabled routes because a connector cannot see `sensing_routes`. `armed:false` is the fleet default and the only value any connector has yet seen. A person detector cannot vouch for an arbitrary route — on frames it graded `empty` the VLM reported `forklift_present`, `fire_or_smoke_visible` and `spill_on_floor` true — so a screen is refused unless every enabled route is a PRESENCE condition over `people_present`/`person_count`; a condition satisfied BY the empty reading (`people_present eq false`) is refused too, since an `empty` verdict is exactly what would have fired it. `reason` carries which gate refused, so an operator asking why their box still pays for every frame gets an answer from this body alone. See docs/runbooks/cctv-detector-cascade-arming.md.",
            "example": {
              "armed": false,
              "reason": "mode_off"
            },
            "properties": {
              "armed": {
                "type": "boolean"
              },
              "reason": {
                "type": "string"
              }
            },
            "required": [
              "armed",
              "reason"
            ],
            "type": "object"
          },
          "fieldRouting": {
            "$ref": "#/components/schemas/DeviceApi.SensingProfileFieldRouting"
          },
          "fields": {
            "description": "The operator's vision-observable custom fields, plus one synthetic alert-focus field per saved alert rectangle on this screen. Empty when no profile is deployed, or on a profile-read error.",
            "items": {
              "$ref": "#/components/schemas/DeviceApi.SensingProfileField"
            },
            "type": "array"
          },
          "models": {
            "description": "The models this device may actually receive, sorted by `modelId`. This is the field that makes a SPECIALIST model reachable: the connector registers one OTA download per granted model whose format it can load, writing to the path the on-device service already reads. Before it existed the connector's whole model-delivery surface was one hardcoded `gemma-vlm` type, which is how a registry row could be active with a live canary and zero devices succeeding. EMPTY is a valid answer and means exactly what it says — no model is grantable to this device right now — including on the degraded path, where advertising a model the read could not confirm would be worse than advertising none.",
            "items": {
              "$ref": "#/components/schemas/DeviceApi.SensingProfileModel"
            },
            "type": "array"
          },
          "profileId": {
            "description": "Active profile's id, or null when no profile is deployed.",
            "example": "sp_kitchen",
            "nullable": true,
            "type": "string"
          },
          "profileName": {
            "description": "Active profile's name, or null when no profile is deployed on the screen.",
            "example": "Queue Watch",
            "nullable": true,
            "type": "string"
          },
          "profileType": {
            "description": "The profile's own KIND (`sensing_profiles.profile_type`): `custom_intent` for an operator-authored Sense-Anything profile, or a builtin preset id (`retail_full_v1`, `convenience_store_v1`, …) for a seeded DOOH audience-measurement profile. The connector reads it to pick which DEFAULT field block to request — see trillboards-edge-sdk localSensing.fieldSetForProfileType. Null when no profile is deployed.",
            "example": "custom_intent",
            "nullable": true,
            "type": "string"
          },
          "screenId": {
            "description": "Echo of the sensor the presented device token is bound to (`req.deviceAuth.sensorId`). The caller cannot ask for another screen — there is no screenId in the request.",
            "example": "6a42e56a5c02c396bbb032b7",
            "minLength": 1,
            "type": "string"
          },
          "success": {
            "description": "Always true on 200 — including the no-profile and profile-read-error paths, which return an EMPTY profile rather than an error so a transient failure never silences sensing.",
            "enum": [
              true
            ],
            "type": "boolean"
          },
          "video": {
            "additionalProperties": false,
            "description": "CCTV video recording knobs; present only when the device token is bound to a camera (deviceAuth.sensorKind camera), absent for tablets. Read per request from runtime_config CCTV_VIDEO_DAILY_SECONDS / CCTV_VIDEO_SEGMENT_SECONDS / CCTV_VIDEO_RING_SECONDS (videoCapturePolicy.composeCctvVideoSection): daily 0..86400 (0 = off, 86400 = continuous), segment 10..300, ring [segment, 604800]. The connector records its crop-less persistent feeds into segmentSeconds MP4 segments up to dailySeconds a UTC day and holds at most ringSeconds of footage on disk (converted with the bytes per second it measures).",
            "example": {
              "dailySeconds": 86400,
              "ringSeconds": 3600,
              "segmentSeconds": 60
            },
            "properties": {
              "dailySeconds": {
                "maximum": 86400,
                "minimum": 0,
                "type": "integer"
              },
              "ringSeconds": {
                "maximum": 604800,
                "minimum": 10,
                "type": "integer"
              },
              "segmentSeconds": {
                "maximum": 300,
                "minimum": 10,
                "type": "integer"
              }
            },
            "required": [
              "dailySeconds",
              "segmentSeconds",
              "ringSeconds"
            ],
            "type": "object"
          }
        },
        "required": [
          "success",
          "screenId",
          "profileName",
          "profileId",
          "profileType",
          "captureIntervalMs",
          "fields",
          "fieldRouting",
          "absenceWatchedFields",
          "models",
          "classes",
          "detectorCascade"
        ],
        "type": "object"
      },
      "DeviceApi.SensingProfileWithheldField": {
        "additionalProperties": false,
        "properties": {
          "detail": {
            "description": "Human-readable reason, naming both claimants on a collision.",
            "example": "declared as an output by efficientdet and other_det — the registry cannot say who answers it",
            "type": "string"
          },
          "field": {
            "description": "The field key as the profile declared it. MAY be empty — that is the `malformed` case.",
            "example": "queue_length",
            "type": "string"
          },
          "reason": {
            "description": "`collision` — two active granted models declare this field, so the registry cannot say who answers it. `malformed` — the profile entry carries no usable field name. Kept in lockstep with `modelClassLiveness.FIELD_WITHHOLD_REASONS`.",
            "enum": [
              "collision",
              "malformed"
            ],
            "example": "collision",
            "type": "string"
          }
        },
        "required": [
          "field",
          "reason",
          "detail"
        ],
        "type": "object"
      },
      "DeviceAuthRefusal": {
        "description": "The device-authentication layer's refusal (missing, invalid or replayed device credential).",
        "properties": {
          "code": {
            "description": "The HTTP status.",
            "type": "integer"
          },
          "error": {
            "description": "device_token_required, invalid_device_token, device_not_found, device_identity_mismatch, device_token_subject_mismatch, device_screen_mismatch, missing_device_timestamp, missing_device_nonce, stale_device_request, replayed_device_request or replay_guard_unavailable.",
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "status": {
            "enum": [
              false
            ],
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "DeviceCapabilityPayload": {
        "additionalProperties": true,
        "properties": {
          "agentType": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "agentVersion": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "audienceSensing": {
            "additionalProperties": true,
            "description": "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.",
            "nullable": true,
            "properties": {
              "cameraAvailable": {
                "nullable": true,
                "type": "boolean"
              },
              "cameraCount": {
                "nullable": true,
                "type": "integer"
              },
              "cameraHealth": {
                "additionalProperties": true,
                "nullable": true,
                "properties": {
                  "lastFailureClass": {
                    "nullable": true,
                    "type": "string"
                  },
                  "lastFailureMessage": {
                    "nullable": true,
                    "type": "string"
                  },
                  "lastFrameAtMs": {
                    "nullable": true,
                    "type": "integer"
                  },
                  "retryCount": {
                    "nullable": true,
                    "type": "integer"
                  },
                  "state": {
                    "nullable": true,
                    "type": "string"
                  }
                },
                "type": "object"
              },
              "cameraType": {
                "nullable": true,
                "type": "string"
              },
              "microphoneAvailable": {
                "nullable": true,
                "type": "boolean"
              },
              "sensingMode": {
                "nullable": true,
                "type": "string"
              }
            },
            "type": "object"
          },
          "audioControl": {
            "additionalProperties": true,
            "description": "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.",
            "nullable": true,
            "properties": {
              "mute": {
                "nullable": true,
                "type": "boolean"
              },
              "volume": {
                "nullable": true,
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "capabilitiesVersion": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "environmentSensors": {
            "additionalProperties": true,
            "description": "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.",
            "nullable": true,
            "properties": {
              "ambientLight": {
                "nullable": true,
                "type": "boolean"
              },
              "temperature": {
                "nullable": true,
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "inputControl": {
            "additionalProperties": true,
            "description": "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.",
            "nullable": true,
            "properties": {
              "appSwitch": {
                "nullable": true,
                "type": "boolean"
              },
              "hdmi": {
                "nullable": true,
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "isDeviceOwner": {
            "description": "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.",
            "nullable": true,
            "type": "boolean"
          },
          "managementCapabilities": {
            "additionalProperties": true,
            "description": "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.",
            "nullable": true,
            "properties": {
              "clearCache": {
                "nullable": true,
                "type": "boolean"
              },
              "installApk": {
                "nullable": true,
                "type": "boolean"
              },
              "kioskMode": {
                "nullable": true,
                "type": "boolean"
              },
              "reboot": {
                "nullable": true,
                "type": "boolean"
              },
              "restart": {
                "nullable": true,
                "type": "boolean"
              },
              "screenshot": {
                "nullable": true,
                "type": "boolean"
              },
              "wipe": {
                "nullable": true,
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "mlCapabilities": {
            "additionalProperties": true,
            "description": "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.",
            "nullable": true,
            "properties": {
              "availableRamMb": {
                "nullable": true,
                "type": "integer"
              },
              "chipsetName": {
                "nullable": true,
                "type": "string"
              },
              "chipsetVendor": {
                "nullable": true,
                "type": "string"
              },
              "cpuAbi": {
                "nullable": true,
                "type": "string"
              },
              "densityDpi": {
                "nullable": true,
                "type": "integer"
              },
              "gpuDelegateSupported": {
                "nullable": true,
                "type": "boolean"
              },
              "gpuName": {
                "nullable": true,
                "type": "string"
              },
              "hasNpu": {
                "nullable": true,
                "type": "boolean"
              },
              "maxVlmSizeMb": {
                "nullable": true,
                "type": "integer"
              },
              "nnapiSupported": {
                "nullable": true,
                "type": "boolean"
              },
              "npuName": {
                "nullable": true,
                "type": "string"
              },
              "osApiLevel": {
                "nullable": true,
                "type": "integer"
              },
              "recommendedModelTier": {
                "nullable": true,
                "type": "string"
              },
              "screenHeightPx": {
                "nullable": true,
                "type": "integer"
              },
              "screenWidthPx": {
                "nullable": true,
                "type": "integer"
              },
              "totalRamMb": {
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "powerControl": {
            "additionalProperties": true,
            "description": "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.",
            "nullable": true,
            "properties": {
              "cec": {
                "nullable": true,
                "type": "boolean"
              },
              "softBlackout": {
                "nullable": true,
                "type": "boolean"
              },
              "wol": {
                "nullable": true,
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "wifiCsiSensing": {
            "additionalProperties": true,
            "description": "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.",
            "nullable": true,
            "properties": {
              "csiAvailable": {
                "nullable": true,
                "type": "boolean"
              },
              "csiHardwareType": {
                "nullable": true,
                "type": "string"
              },
              "csiNodeCount": {
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "DeviceSummary": {
        "properties": {
          "created_at": {
            "description": "Creation time of the DEVICE row, not of the linked screen (which can be created later).",
            "format": "date-time",
            "type": "string"
          },
          "device_id": {
            "description": "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`.",
            "type": "string"
          },
          "device_type": {
            "description": "Saved mutable partner category, independent of advertising classification. Legacy devices without a saved declaration retain their previous numeric-derived label until explicitly updated.",
            "type": "string"
          },
          "display": {
            "description": "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.",
            "type": "object"
          },
          "embed_url": {
            "description": "Static, unsigned player URL `https://screen.trillboards.com?fp=<fingerprint>`. It never expires or rotates, and is `null` until a screen is linked.",
            "type": "string"
          },
          "enrichment": {
            "description": "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.",
            "type": "object"
          },
          "external_device_id": {
            "description": "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.",
            "type": "string"
          },
          "fingerprint": {
            "description": "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.",
            "type": "string"
          },
          "location": {
            "description": "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.",
            "type": "object"
          },
          "location_capability": {
            "$ref": "#/components/schemas/PartnerLocationCapability"
          },
          "monetization_status": {
            "description": "Stripe-style readiness, the same contract registration returns (see https://api.trillboards.com/docs/monetization-requirements).",
            "enum": [
              "ready",
              "limited",
              "blocked"
            ],
            "type": "string"
          },
          "name": {
            "description": "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>`.",
            "type": "string"
          },
          "requirements": {
            "$ref": "#/components/schemas/PartnerMonetizationRequirements"
          },
          "screen_id": {
            "description": "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.",
            "type": "string"
          },
          "status": {
            "description": "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.",
            "type": "string"
          },
          "updated_at": {
            "description": "`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.",
            "format": "date-time",
            "type": "string"
          },
          "venue": {
            "description": "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.",
            "type": "object"
          }
        },
        "type": "object"
      },
      "DisplaySettingsInput": {
        "properties": {
          "ad_duration_default": {
            "description": "Default ad duration in seconds",
            "type": "integer"
          },
          "brightness": {
            "description": "Screen brightness percentage",
            "maximum": 100,
            "minimum": 0,
            "type": "integer"
          },
          "content_fit": {
            "$ref": "#/components/schemas/ContentFitSettings"
          },
          "display_mode": {
            "description": "Ad display mode",
            "enum": [
              "fullscreen",
              "l-bar",
              "split"
            ],
            "type": "string"
          },
          "idle_content": {
            "description": "What to show when no ads",
            "enum": [
              "default_stream",
              "black",
              "logo"
            ],
            "type": "string"
          },
          "mute": {
            "description": "Mute audio",
            "type": "boolean"
          },
          "orientation": {
            "description": "Screen orientation",
            "enum": [
              "landscape",
              "portrait",
              "auto"
            ],
            "type": "string"
          },
          "qr_code_position": {
            "description": "QR code position on screen",
            "enum": [
              "bottom-left",
              "bottom-right",
              "top-left",
              "top-right",
              "none"
            ],
            "type": "string"
          },
          "transition_effect": {
            "description": "Transition effect between ads",
            "enum": [
              "fade",
              "slide",
              "none"
            ],
            "type": "string"
          },
          "volume": {
            "description": "Audio volume percentage",
            "maximum": 100,
            "minimum": 0,
            "type": "integer"
          }
        },
        "type": "object"
      },
      "DisplaySettingsResponse": {
        "properties": {
          "ad_duration_default": {
            "description": "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).",
            "type": "integer"
          },
          "ad_surfaces": {
            "description": "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.",
            "items": {
              "properties": {
                "height_percent": {
                  "type": "number"
                },
                "id": {
                  "type": "string"
                },
                "position": {
                  "type": "string"
                },
                "width_percent": {
                  "type": "number"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "brightness": {
            "description": "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.",
            "type": "integer"
          },
          "content_fit": {
            "$ref": "#/components/schemas/ContentFitSettings"
          },
          "display_mode": {
            "description": "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`.",
            "type": "string"
          },
          "idle_content": {
            "description": "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.",
            "type": "string"
          },
          "mute": {
            "description": "NOT EMITTED and NOT WRITABLE. Use `sound_enabled: false` instead — that key the formatter does return, defaulting to `true`.",
            "type": "boolean"
          },
          "object": {
            "description": "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.",
            "example": "display_settings",
            "type": "string"
          },
          "orientation": {
            "description": "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.",
            "type": "string"
          },
          "playback_policy": {
            "description": "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`.",
            "properties": {
              "allow_skip": {
                "type": "boolean"
              },
              "max_duration_seconds": {
                "type": "integer"
              },
              "min_break_between_ads": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "qr_code_position": {
            "description": "NOT EMITTED and NOT WRITABLE — the identifier does not occur anywhere in the API source.",
            "type": "string"
          },
          "request_id": {
            "description": "Echo of the server-assigned `req.requestId`, attached only when one exists — the key is absent, not null, otherwise.",
            "type": "string"
          },
          "screen_id": {
            "description": "Echoed from the `{screenId}` path parameter rather than read from a column.",
            "type": "string"
          },
          "transition_effect": {
            "description": "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.",
            "type": "string"
          },
          "updated_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "volume": {
            "description": "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`.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "EarnerScreens": {
        "description": "User-owned ad screens. Each row is a logical screen that can display ads. Links user to device. PG primary with Mongo legacy mirror.",
        "properties": {
          "ad_interval_seconds": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "ads_disabled_reasons": {
            "example": [],
            "items": {
              "type": "string"
            },
            "type": "array",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "advertisement_sizing": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "archetype_photo_key": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-08-18T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "archetype_photo_synced_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-08-18T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "archetype_photo_url": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-08-18T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "auction_cohort": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "auction_enabled_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "auction_mode": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "auction_type": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "avg_dwell_time_seconds": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "back_online_alert_sent_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-04T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "billing_mode": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "block_group_geoid": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-07-04T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "blocked_advertisers": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "blocked_iab_categories": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "brand_substitution_consent_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "brand_substitution_consent_by": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "brand_substitution_mode": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "branding_overlay_enabled": {
            "example": false,
            "type": "boolean",
            "x-since": "2026-04-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "branding_overlay_hide_on_ads": {
            "example": false,
            "type": "boolean",
            "x-since": "2026-04-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "branding_overlay_logo_transformed_url": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-04-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "branding_overlay_logo_url": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-04-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "branding_overlay_opacity": {
            "description": "NUMERIC(5,4).",
            "example": 1.5,
            "type": "number",
            "x-since": "2026-04-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "branding_overlay_padding_ratio": {
            "description": "NUMERIC(5,4).",
            "example": 1.5,
            "type": "number",
            "x-since": "2026-04-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "branding_overlay_placement": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-07-16T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "branding_overlay_position": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-04-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "branding_overlay_size_ratio": {
            "description": "NUMERIC(5,4).",
            "example": 1.5,
            "type": "number",
            "x-since": "2026-04-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "cms_payment_status": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "commercial_value_score": {
            "example": 1.5,
            "format": "double",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "content_filtering_enabled": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "deleted_at": {
            "description": "deleted_at IS NULL required — soft-delete nulls earnerscreen_device_id for partial unique index",
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "delivery_mode": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-04-12T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "device_platform_override": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-15T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "diagonal_inches": {
            "example": 1.5,
            "format": "double",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "display_mode": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "distributor_id": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "dma_code": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-07-04T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "dma_name": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-07-04T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "dynamic_floor_pricing": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "enrichment_attempts": {
            "example": 0,
            "format": "int32",
            "type": "integer",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "enrichment_failed_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-05T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "environment_type": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "floor_price_cpm": {
            "description": "NUMERIC(12,6).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "foot_traffic_multiplier": {
            "example": 1.5,
            "format": "double",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "gam_ad_unit_code": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "gam_ad_unit_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "gam_ad_unit_path": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "gam_sync_status": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "gam_synced_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "geocode_source": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "geocoded_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-22T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "geocoding_raw_response": {
            "example": {},
            "nullable": true,
            "type": "object",
            "x-since": "2026-05-22T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "hb_allocation": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "hb_enabled": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "hb_force_request": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "hb_last_sync_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "hb_min_interval_seconds": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "hb_provider": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "hb_request_mode": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "hb_timeout_ms": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "hb_vast_tag_url": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "height_inches": {
            "example": 1.5,
            "format": "double",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "hive_stack_screen_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "hive_stack_site_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "hive_stack_sync_status": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "hive_stack_synced_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "hive_stack_unit_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "hive_stack_unit_uuid": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "is_demo_data": {
            "example": false,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "is_standby": {
            "example": false,
            "type": "boolean",
            "x-since": "2026-04-08T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "l_bar_ads_enabled": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "l_bar_position": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-06-22T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "last_enrichment_attempt_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "last_offline_transition_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-08T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "last_programmatic_sync_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "latitude": {
            "example": 1.5,
            "format": "double",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "longitude": {
            "example": 1.5,
            "format": "double",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "loop_plan_id": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-04-12T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "media_capability": {
            "enum": [
              "image_video",
              "image_only",
              "video_only"
            ],
            "example": "image_video",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "enum"
          },
          "mediation_priority": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "mobile_distance_threshold_m": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "mobile_update_interval_s": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "mobile_vehicle_type": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "neighborhood": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-07-05T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "ntp_sync_required": {
            "example": false,
            "type": "boolean",
            "x-since": "2026-04-12T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "offline_alert_sent_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-04T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "offline_push_alert_sent_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-09-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "operating_hours": {
            "example": {},
            "nullable": true,
            "type": "object",
            "x-since": "2026-05-12T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "partner_mongo_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "peak_hours": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "photo_flag_notes": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "photo_flag_reason": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "photo_flagged": {
            "example": false,
            "type": "boolean",
            "x-since": "2026-05-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "photo_flagged_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "photo_flagged_by_mongo_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "photo_notification_count": {
            "example": 0,
            "format": "int32",
            "type": "integer",
            "x-since": "2026-05-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "photo_notification_sent_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "photo_quality_sent_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-26T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "photo_review_status": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "photos": {
            "example": {},
            "nullable": true,
            "type": "object",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "physical_size_set_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-02T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "physical_size_set_by": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-02T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "place_name": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-07-05T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "placement_type": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "places_rating": {
            "example": 1.5,
            "format": "double",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "places_review_count": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "pref_allow_religious": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "pref_allow_tobacco": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "pref_allowed_categories": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "pref_allowed_keywords": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "pref_allowed_subjects": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "pref_auto_approve_known_brands": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "pref_blocked_categories": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "pref_blocked_keywords": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "pref_blocked_subjects": {
            "example": [],
            "items": {
              "type": "string"
            },
            "nullable": true,
            "type": "array",
            "x-since": "2026-04-10T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "pref_level": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "pref_max_age_rating": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "preferred_deal_ids": {
            "example": [],
            "items": {
              "type": "string"
            },
            "type": "array",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "prog_allocation_pct": {
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "prog_currency": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "programmatic_enabled": {
            "example": false,
            "type": "boolean",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "region": {
            "example": "sample",
            "type": "string",
            "x-since": "2026-05-02T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "registration_source": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-08T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "screen_placement": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "serving_tenant_id": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "slot_duration_seconds": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-12T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "sound_enabled": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "ssp_enabled": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "state_iso_3166_2": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-22T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "sync_enabled": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-since": "2026-04-06T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "sync_group_name": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-06T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "sync_is_master": {
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-since": "2026-04-06T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "boolean"
          },
          "sync_last_sync_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-06T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "sync_master_screen_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-06T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "taggify_screen_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "taxonomy_migrated_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "taxonomy_spec_version": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_category": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_child": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_child_id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_city": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_classification_source": {
            "description": "enrichment_status: PG GENERATED from (venue_classification_source, enrichment_failed_at); a real classification DOMINATES a stale failure stamp (success wins). Never written directly. Values: pendi...",
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-14T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_grandchild": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_parent": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_state": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-11T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "venue_subcategory": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "text"
          },
          "waterfall_optimized_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "waterfall_partners": {
            "description": "waterfall_partners is the ONLY pin store (order + whitelist; screen_partner_settings was never built). Serving reads it only with a screen_header_bidding_config row and ignores it once waterfall_op...",
            "example": [],
            "items": {
              "type": "string"
            },
            "type": "array",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          },
          "waterfall_total_timeout_ms": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-04-21T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml",
            "x-units": "count"
          },
          "width_inches": {
            "description": "Physical width_inches/height_inches/diagonal_inches are inches. Pixels live on earner_screen_devices; the raw screen has no resolution_width/resolution_height columns.",
            "example": 1.5,
            "format": "double",
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
          }
        },
        "required": [
          "is_standby",
          "delivery_mode",
          "ntp_sync_required",
          "waterfall_partners",
          "programmatic_enabled",
          "prog_currency",
          "preferred_deal_ids",
          "branding_overlay_enabled",
          "branding_overlay_logo_url",
          "branding_overlay_logo_transformed_url",
          "branding_overlay_position",
          "branding_overlay_size_ratio",
          "branding_overlay_padding_ratio",
          "branding_overlay_opacity",
          "branding_overlay_hide_on_ads",
          "region",
          "photo_flagged",
          "photo_notification_count",
          "branding_overlay_placement",
          "is_demo_data",
          "billing_mode",
          "ads_disabled_reasons",
          "cms_payment_status",
          "brand_substitution_mode",
          "enrichment_attempts"
        ],
        "type": "object",
        "x-sor-ref": "docs/schemas/earners-supply/earner_screens.yaml"
      },
      "EarningsBreakdownResponse": {
        "properties": {
          "breakdown": {
            "description": "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`.",
            "items": {
              "properties": {
                "date": {
                  "description": "Date or screen_id depending on group_by",
                  "format": "date",
                  "type": "string"
                },
                "earning_sources": {
                  "$ref": "#/components/schemas/EarningsSources"
                },
                "earnings_cents": {
                  "type": "integer"
                },
                "impressions": {
                  "type": "integer"
                },
                "screen_id": {
                  "description": "Screen ID (when grouped by screen)",
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "pending_programmatic": {
            "description": "**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.",
            "properties": {
              "confirmation_date": {
                "format": "date",
                "nullable": true,
                "type": "string"
              },
              "estimated_cents": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "period": {
            "description": "`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.",
            "properties": {
              "end": {
                "format": "date-time",
                "type": "string"
              },
              "group_by": {
                "enum": [
                  "day",
                  "week",
                  "screen"
                ],
                "type": "string"
              },
              "start": {
                "format": "date-time",
                "type": "string"
              }
            },
            "type": "object"
          },
          "summary": {
            "description": "Integer US cents for the selected period. With source reporting enabled, direct_cents is verified first-party revenue and programmatic_cents is the remainder; earning_sources supplies the business classification. Disabled reporting retains the legacy split.",
            "properties": {
              "direct_cents": {
                "type": "integer"
              },
              "earning_source_version": {
                "enum": [
                  1
                ],
                "type": "integer"
              },
              "earning_sources": {
                "$ref": "#/components/schemas/EarningsSources"
              },
              "programmatic_cents": {
                "type": "integer"
              },
              "total_cents": {
                "type": "integer"
              },
              "total_impressions": {
                "type": "integer"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "EarningsData": {
        "description": "Partner earnings summary with optional daily series\n(`@trillboards/api-client` `EarningsData`).\n",
        "properties": {
          "average_cpm": {
            "description": "**Never emitted.** No partner earnings endpoint computes a CPM; the earnings chart returns impressions and cents only, so a caller must divide for itself.",
            "type": "number"
          },
          "daily": {
            "description": "Optional per-day earnings/impressions series.",
            "items": {
              "properties": {
                "date": {
                  "description": "Day label (YYYY-MM-DD).",
                  "type": "string"
                },
                "earnings": {
                  "type": "number"
                },
                "impressions": {
                  "type": "number"
                }
              },
              "required": [
                "date",
                "earnings",
                "impressions"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "last_payout_amount": {
            "description": "**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.",
            "type": "number"
          },
          "last_payout_date": {
            "description": "**Never emitted.** No handler populates it — read `GET /v1/partner/earnings/transactions?type=payout`, whose rows carry `created_at` and `amount_cents`.",
            "format": "date-time",
            "type": "string"
          },
          "pending_payout": {
            "description": "Balance not yet paid out.",
            "type": "number"
          },
          "period": {
            "description": "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.",
            "type": "string"
          },
          "total_earnings": {
            "description": "**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`.",
            "type": "number"
          },
          "total_impressions": {
            "description": "**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.",
            "type": "number"
          }
        },
        "required": [
          "period",
          "total_earnings",
          "total_impressions",
          "average_cpm",
          "pending_payout"
        ],
        "type": "object"
      },
      "EarningsResponse": {
        "properties": {
          "available_for_payout_cents": {
            "description": "Amount available for immediate payout",
            "type": "integer"
          },
          "breakdown": {
            "description": "Current-month integer US cents. With source reporting enabled, direct_campaigns is verified first-party revenue and programmatic_google is the remaining aggregate, not Google-specific. programmatic_floodgates remains zero. Prefer earning_sources; disabled reporting retains the legacy all-programmatic split.",
            "properties": {
              "direct_campaigns": {
                "type": "integer"
              },
              "programmatic_floodgates": {
                "type": "integer"
              },
              "programmatic_google": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "earning_source_version": {
            "enum": [
              1
            ],
            "type": "integer"
          },
          "earning_sources": {
            "$ref": "#/components/schemas/EarningsSources"
          },
          "next_payout_estimate": {
            "description": "`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.",
            "properties": {
              "amount_cents": {
                "type": "integer"
              },
              "date": {
                "format": "date",
                "type": "string"
              },
              "stripe_status": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "payout_threshold_cents": {
            "description": "Minimum amount required for payout",
            "type": "integer"
          },
          "pending_confirmation_cents": {
            "description": "Programmatic earnings pending Net-60 confirmation",
            "type": "integer"
          },
          "revenue_share_percent": {
            "description": "Partner's revenue share percentage",
            "type": "number"
          },
          "this_month_cents": {
            "description": "Estimated earnings for current month",
            "type": "integer"
          },
          "total_lifetime_cents": {
            "description": "Total earnings since partner started",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "EarningsSources": {
        "description": "Optional reporting breakdown in integer US cents. Components sum to the existing amount. Missing evidence is Unclassified; absence of this object means reporting is unavailable.",
        "properties": {
          "direct_bookings": {
            "description": "Paid first-party portal and offline IO bookings.",
            "type": "integer"
          },
          "open_exchange": {
            "description": "Verified open-auction delivery outside a private deal.",
            "type": "integer"
          },
          "programmatic_deals": {
            "description": "Verified delivered DSP/SSP PMP, PG and preferred deals.",
            "type": "integer"
          },
          "unclassified": {
            "description": "Earnings lacking supported allocation evidence.",
            "type": "integer"
          }
        },
        "required": [
          "direct_bookings",
          "programmatic_deals",
          "open_exchange",
          "unclassified"
        ],
        "type": "object"
      },
      "Error": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LegacyError"
          }
        ],
        "description": "Legacy free-text error shape. COLLAPSED onto the shared `LegacyError`\ndefinition so there is exactly one declaration of this shape across the\nspecs. Kept as a distinct NAME because hand-authored operations\nreference it by that name; the shape is unchanged.\n",
        "x-superseded-by": "TrillboardsError"
      },
      "ErrorCode": {
        "description": "The 143 CATALOGUED error codes, generated from utils/errorCatalog.js — the same catalog that backs https://api.trillboards.com/docs/errors. This is the documented registry, NOT the complete set of codes the API can emit: a response `code` is an open string (see `TrillboardsError`). Use this list to recognise a code, never to assume a response cannot carry another.",
        "enum": [
          "AUTH_REQUIRED",
          "BRAND_CONFIGURATION_FAILED",
          "BRAND_PROFILE_CONFLICT",
          "BRAND_PROFILE_NOT_FOUND",
          "CREATIVE_ADVERTISEMENT_NOT_OWNED",
          "CREATIVE_MODERATION_UNAVAILABLE",
          "CREATIVE_NOT_LINKED",
          "CREATIVE_NOT_READY",
          "CREATIVE_POLICY_CONFLICT",
          "CREATIVE_TOO_LARGE",
          "EXPERIMENT_COMPLETED",
          "EXPERIMENT_NOT_FOUND",
          "FEEDBACK_POLICY_REQUIRED",
          "IDEMPOTENCY_CONFLICT",
          "INVALID_BRAND_RULES",
          "INVALID_CREATIVE_URL",
          "INVALID_CREATIVE_WEIGHTS",
          "INVALID_FEEDBACK_CREATIVE",
          "INVALID_FEEDBACK_OBJECTIVE",
          "INVALID_FEEDBACK_REQUEST",
          "INVALID_POLICY_VERSION",
          "INVALID_VARIANTS",
          "MEDIA_BUY_NOT_FOUND",
          "OPERATION_ALREADY_COMMITTED",
          "OPERATION_IN_PROGRESS",
          "POLICY_NOT_AVAILABLE",
          "SOURCE_CREATIVE_NOT_FOUND",
          "UNSUPPORTED_CREATIVE_TYPE",
          "VARIANT_IDENTITY_CONFLICT",
          "ad_interval_out_of_range",
          "allocation_out_of_range",
          "analytics_unavailable",
          "api_key_expired",
          "api_key_owner_missing",
          "api_key_revoked",
          "array_items_must_be_strings",
          "array_too_large",
          "auth_error",
          "authentication_required",
          "batch_size_exceeded",
          "blocked_advertisers_too_large",
          "blocked_iab_categories_too_large",
          "client_ip_required",
          "concurrent_request_limit",
          "conflict",
          "conflicting_lists",
          "creative_policy_conflict",
          "creative_policy_forbidden",
          "daily_quota_exceeded",
          "database_error",
          "dataset_not_scopable",
          "device_already_registered",
          "device_identity_missing",
          "device_identity_underivable",
          "device_not_found",
          "device_offline",
          "dsp_access_denied",
          "error",
          "field_must_be_array",
          "field_must_be_boolean",
          "field_too_long",
          "floor_price_out_of_range",
          "forbidden",
          "idempotency_error",
          "idempotency_key_in_use",
          "idempotency_key_mismatch",
          "insufficient_data",
          "insufficient_permissions",
          "insufficient_scope",
          "internal_error",
          "invalid_ad_surface_mode",
          "invalid_age_rating",
          "invalid_api_key",
          "invalid_api_key_format",
          "invalid_array_items",
          "invalid_creative_policy",
          "invalid_currency",
          "invalid_dataset",
          "invalid_device_id",
          "invalid_dimensions",
          "invalid_display_mode",
          "invalid_field_type",
          "invalid_field_value",
          "invalid_fingerprint",
          "invalid_json",
          "invalid_lbar_position",
          "invalid_lbar_settings",
          "invalid_measures",
          "invalid_media_url",
          "invalid_order_by",
          "invalid_orientation",
          "invalid_pip_position",
          "invalid_pip_settings",
          "invalid_preference_level",
          "invalid_preferences",
          "invalid_provider",
          "invalid_screen_id",
          "invalid_segment_rules",
          "invalid_sizing_mode",
          "invalid_stream_type",
          "invalid_webhook_events",
          "invalid_webhook_url",
          "lbar_settings_must_be_object",
          "lbar_size_out_of_range",
          "missing_required_field",
          "not_found",
          "openrtb_auth_failed",
          "openrtb_config_missing",
          "paired_training_insert_failed",
          "paired_training_rate_limit_exceeded",
          "partner_inactive",
          "pip_margin_out_of_range",
          "pip_settings_must_be_object",
          "pip_size_ratio_out_of_range",
          "preferences_must_be_object",
          "preferences_required",
          "preset_id_required",
          "preset_not_found",
          "rate_limit_exceeded",
          "resource_locked",
          "screen_ids_required",
          "screen_not_found",
          "screen_not_in_scope",
          "sdk_enrich_rate_limit_exceeded",
          "sdk_impression_rate_limit_exceeded",
          "sdk_qr_beacon_rate_limit_exceeded",
          "sdk_qr_scan_rate_limit_exceeded",
          "segment_limit_exceeded",
          "segment_not_found",
          "service_unavailable",
          "stale_data",
          "stream_content_required",
          "stream_not_found",
          "tier_insufficient",
          "timeout",
          "timeout_out_of_range",
          "unknown_agent_type",
          "unsupported_media_type",
          "validation_error",
          "webhook_limit_exceeded",
          "webhook_not_found",
          "webhook_secret_expired",
          "webhook_url_unreachable"
        ],
        "type": "string"
      },
      "ErrorCodeResponse": {
        "properties": {
          "code": {
            "example": "not_found",
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "doc_url": {
            "format": "uri",
            "type": "string"
          },
          "example_response": {
            "description": "Example canonical error envelope for this code.",
            "properties": {
              "error": {
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "doc_url": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  },
                  "param": {},
                  "type": {
                    "type": "string"
                  }
                },
                "required": [
                  "type",
                  "code",
                  "message",
                  "doc_url"
                ],
                "type": "object"
              }
            },
            "required": [
              "error"
            ],
            "type": "object"
          },
          "http_status": {
            "example": 404,
            "type": "number"
          },
          "object": {
            "description": "Always \"error_code\".",
            "enum": [
              "error_code"
            ],
            "type": "string"
          },
          "type": {
            "example": "invalid_request_error",
            "type": "string"
          }
        },
        "required": [
          "object",
          "code",
          "type",
          "http_status",
          "description",
          "doc_url",
          "example_response"
        ],
        "type": "object"
      },
      "ErrorEntry": {
        "description": "A single partner API error-log record\n(`@trillboards/api-client` `ErrorEntry`) — one logged error occurrence\nscoped to the partner. Distinct from the public error *catalog* entries\n(`ErrorListItem` / `ErrorCodeResponse`), which describe error codes in\nthe abstract.\n",
        "properties": {
          "code": {
            "description": "Stable machine error code (e.g. not_found).",
            "type": "string"
          },
          "doc_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          },
          "endpoint": {
            "description": "The API path that produced the error.",
            "type": "string"
          },
          "id": {
            "description": "**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`.",
            "type": "string"
          },
          "message": {
            "description": "**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.",
            "type": "string"
          },
          "method": {
            "description": "The HTTP method of the failing request.",
            "type": "string"
          },
          "param": {
            "description": "The request parameter the error concerns, if any.",
            "type": "string"
          },
          "request_id": {
            "description": "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.",
            "type": "string"
          },
          "timestamp": {
            "description": "**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.",
            "format": "date-time",
            "type": "string"
          },
          "type": {
            "description": "Stripe-style error taxonomy (matches the SDK's ApiErrorType).",
            "enum": [
              "invalid_request_error",
              "authentication_error",
              "permission_error",
              "rate_limit_error",
              "idempotency_error",
              "api_error"
            ],
            "type": "string"
          }
        },
        "required": [
          "id",
          "code",
          "type",
          "message",
          "doc_url",
          "timestamp",
          "request_id",
          "endpoint",
          "method"
        ],
        "type": "object"
      },
      "ErrorEnvelope": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/TrillboardsError"
          },
          {
            "$ref": "#/components/schemas/HybridError"
          },
          {
            "$ref": "#/components/schemas/LegacyCodedError"
          },
          {
            "$ref": "#/components/schemas/LegacyError"
          }
        ],
        "description": "The complete set of error envelopes this API may return. Documented on operations whose surface emits more than one — see `x-superseded-by` on each member for the migration target. New endpoints return `TrillboardsError`; switch on `error.code`, never on a message string.",
        "type": "object",
        "x-envelope-set": "error"
      },
      "ErrorListItem": {
        "properties": {
          "code": {
            "example": "not_found",
            "type": "string"
          },
          "description": {
            "example": "The requested resource does not exist.",
            "type": "string"
          },
          "doc_url": {
            "example": "https://api.trillboards.com/docs/errors#not_found",
            "format": "uri",
            "type": "string"
          },
          "http_status": {
            "example": 404,
            "type": "number"
          },
          "type": {
            "example": "invalid_request_error",
            "type": "string"
          }
        },
        "required": [
          "code",
          "type",
          "http_status",
          "description",
          "doc_url"
        ],
        "type": "object"
      },
      "ErrorListResponse": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/ErrorListItem"
            },
            "type": "array"
          },
          "object": {
            "description": "Always \"list\".",
            "enum": [
              "list"
            ],
            "type": "string"
          },
          "total": {
            "description": "Total number of error codes.",
            "type": "integer"
          }
        },
        "required": [
          "object",
          "data",
          "total"
        ],
        "type": "object"
      },
      "ErrorType": {
        "description": "Stripe-style error taxonomy. Derived from the distinct `type` values in utils/errorCatalog.js (6 values).",
        "enum": [
          "api_error",
          "authentication_error",
          "idempotency_error",
          "invalid_request_error",
          "permission_error",
          "rate_limit_error"
        ],
        "type": "string"
      },
      "Error__sdk": {
        "properties": {
          "error": {
            "properties": {
              "code": {
                "description": "Stable machine-readable error code. Switch on this, never on `message`. The 143 CATALOGUED codes are published as the `ErrorCode` schema and served live by `GET /v1/errors`, which is the authority. This field is NOT limited to them: services emit further codes directly, and `ApiError.notFound(resource)` mints `<resource>_not_found` from an arbitrary resource name. Always handle an unrecognised code by falling back to `type`.",
                "example": "not_found",
                "type": "string",
                "x-error-code-catalog": "#/components/schemas/ErrorCode"
              },
              "details": {
                "description": "Per-field violations (populated from Zod errors).",
                "items": {
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "param": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                },
                "type": "array"
              },
              "doc_url": {
                "description": "Link to the documentation for this error code (https://api.trillboards.com/docs/errors#<code>).",
                "format": "uri",
                "type": "string"
              },
              "help": {
                "description": "Agent-facing hints and suggestions.",
                "properties": {
                  "portal_url": {
                    "format": "uri",
                    "type": "string"
                  },
                  "suggestions": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "summary": {
                    "type": "string"
                  },
                  "try_next": {
                    "type": "string"
                  }
                },
                "type": "object"
              },
              "message": {
                "description": "Human-readable error message. Safe to surface to end-users.",
                "type": "string"
              },
              "param": {
                "description": "Offending parameter name (when applicable).",
                "type": "string"
              },
              "request_id": {
                "description": "Echoed X-Request-Id for correlation.",
                "pattern": "^req_[a-f0-9]{32}$",
                "type": "string"
              },
              "type": {
                "description": "Stripe-style error taxonomy (derived from utils/errorCatalog).",
                "enum": [
                  "api_error",
                  "authentication_error",
                  "idempotency_error",
                  "invalid_request_error",
                  "permission_error",
                  "rate_limit_error"
                ],
                "type": "string"
              }
            },
            "required": [
              "type",
              "code",
              "message",
              "request_id"
            ],
            "type": "object"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "ExtendedIdentity": {
        "description": "Exact DSP-matchable audience identity kept separate from the resettable\nOS advertising ID. `hem_sha256` is an existing SHA-256 digest: send it\ndirectly (the server lowercases hexadecimal once and never hashes it\nagain). `uid2_token` is an opaque, case-sensitive UID2 advertising\ntoken: send it exactly as issued and include `expires_at` so it can be\nrefreshed; tokens contain no whitespace. Use `unavailable` with no\n`id` to clear a prior value.\n",
        "properties": {
          "atype": {
            "description": "OpenRTB person-based identifier type.",
            "enum": [
              3
            ],
            "type": "integer"
          },
          "expires_at": {
            "description": "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\".",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "description": "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.",
            "maxLength": 4096,
            "minLength": 1,
            "nullable": true,
            "pattern": "^\\S+$",
            "type": "string"
          },
          "id_type": {
            "description": "`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.",
            "enum": [
              "hem_sha256",
              "uid2_token"
            ],
            "type": "string"
          },
          "observed_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "source": {
            "description": "Canonical top-level domain for this identity system.",
            "example": "uidapi.com",
            "maxLength": 253,
            "minLength": 1,
            "pattern": "^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}$",
            "type": "string"
          },
          "source_version": {
            "description": "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.",
            "maxLength": 128,
            "nullable": true,
            "type": "string"
          },
          "status": {
            "description": "`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.",
            "enum": [
              "available",
              "unavailable"
            ],
            "type": "string"
          }
        },
        "required": [
          "status",
          "id_type",
          "source",
          "atype",
          "observed_at"
        ],
        "type": "object"
      },
      "FederatedApi.GradientUploadPayload": {
        "additionalProperties": false,
        "properties": {
          "daypart": {
            "maxLength": 32,
            "type": "string"
          },
          "deviceId": {
            "description": "Optional device identifier — Kotlin agent-core also emits this alongside screenId.",
            "maxLength": 64,
            "type": "string"
          },
          "deviceProfile": {
            "maxLength": 64,
            "type": "string"
          },
          "fingerprint": {
            "description": "Device-stable fingerprint for attestation context.",
            "maxLength": 128,
            "type": "string"
          },
          "geo": {
            "maxLength": 32,
            "type": "string"
          },
          "gradientIndices": {
            "description": "Optional indices into the dense parameter vector. When omitted, gradients[] is treated as dense.",
            "items": {
              "minimum": 0,
              "type": "integer"
            },
            "maxItems": 200000,
            "type": "array"
          },
          "gradients": {
            "description": "Sparsified gradient values (top-K-by-magnitude). Aligned 1:1 with gradientIndices when present.",
            "items": {
              "type": "number"
            },
            "maxItems": 200000,
            "minItems": 1,
            "type": "array"
          },
          "localLoss": {
            "description": "Average local training loss across the contributing samples.",
            "type": "number"
          },
          "modelType": {
            "description": "Federated model name (e.g. vas_weights, taste_classifier). Routes the gradient buffer.",
            "example": "vas_weights",
            "maxLength": 64,
            "minLength": 1,
            "type": "string"
          },
          "modelVersion": {
            "description": "Client-side currently-applied model version. Server may signal an update.",
            "minimum": 0,
            "type": "integer"
          },
          "publicKeyId": {
            "description": "Identifier of the Ed25519 public key registered for VAS attestation.",
            "maxLength": 128,
            "type": "string"
          },
          "sampleCount": {
            "description": "Number of training samples contributing to these gradients.",
            "example": 240,
            "minimum": 1,
            "type": "integer"
          },
          "screenId": {
            "description": "Device fingerprint or screen identifier. May also arrive via the X-Device-Fingerprint header.",
            "example": "screen-prod-airport-jfk-t4-001",
            "maxLength": 64,
            "minLength": 8,
            "type": "string"
          },
          "screenMongoId": {
            "description": "Optional MongoDB ObjectId for the bound screen (legacy compatibility).",
            "maxLength": 64,
            "type": "string"
          },
          "signature": {
            "description": "Base64-encoded Ed25519 signature over the canonical gradient payload.",
            "maxLength": 256,
            "type": "string"
          },
          "timestamp": {
            "description": "Unix-ms timestamp captured when the signature was produced.",
            "minimum": 0,
            "type": "integer"
          },
          "venueSubtype": {
            "maxLength": 64,
            "type": "string"
          },
          "venueType": {
            "description": "Venue category for slice aggregation.",
            "example": "transit",
            "maxLength": 64,
            "type": "string"
          }
        },
        "required": [
          "modelType",
          "venueType",
          "gradients",
          "sampleCount"
        ],
        "type": "object"
      },
      "FederatedApi.GradientUploadResponse": {
        "properties": {
          "accepted": {
            "type": "boolean"
          },
          "bufferedCount": {
            "description": "Server-buffered gradient count for this slice after this upload.",
            "minimum": 0,
            "type": "integer"
          },
          "currentModelVersion": {
            "description": "Server-side latest aggregated model version for this slice.",
            "minimum": 0,
            "type": "integer"
          },
          "readyForAggregation": {
            "description": "True when the slice buffer is ready to trigger an aggregation pass.",
            "type": "boolean"
          },
          "sliceKey": {
            "description": "Canonical slice routing key composed from venue/subtype/daypart/geo/profile.",
            "example": "vas_weights|transit|airport|morning|US-NY|premium",
            "type": "string"
          },
          "sliceUpdateAvailable": {
            "description": "True when a slice-specific weight update is ready for the device to fetch.",
            "type": "boolean"
          },
          "success": {
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "accepted",
          "sliceKey",
          "bufferedCount",
          "readyForAggregation"
        ],
        "type": "object"
      },
      "FederatedApi.ModelInfo": {
        "properties": {
          "inherited": {
            "description": "True when the device-requested slice was empty and a parent slice was returned.",
            "type": "boolean"
          },
          "inheritedFrom": {
            "description": "Parent slice key the weights were inherited from (when inherited=true).",
            "nullable": true,
            "type": "string"
          },
          "isDefault": {
            "description": "True when no slice data was available; weights are hard-coded defaults.",
            "type": "boolean"
          },
          "modelType": {
            "type": "string"
          },
          "sampleCount": {
            "minimum": 0,
            "type": "integer"
          },
          "sliceKey": {
            "description": "The slice key whose weights were returned. Null when defaults are served.",
            "nullable": true,
            "type": "string"
          },
          "success": {
            "type": "boolean"
          },
          "updatedAt": {
            "description": "RFC3339 timestamp of the most recent aggregation that produced these weights.",
            "format": "date-time",
            "type": "string"
          },
          "weights": {
            "$ref": "#/components/schemas/FederatedApi.ModelWeights"
          }
        },
        "required": [
          "success",
          "modelType",
          "weights",
          "sliceKey",
          "inherited",
          "sampleCount",
          "isDefault"
        ],
        "type": "object"
      },
      "FederatedApi.ModelLatestDescriptor": {
        "properties": {
          "aggregatedAt": {
            "format": "date-time",
            "type": "string"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "deviceCount": {
            "description": "Distinct device count that contributed to this aggregation pass.",
            "minimum": 0,
            "type": "integer"
          },
          "downloadUrl": {
            "description": "Pre-signed S3 URL for the aggregated weights blob. Populated when an artifact exists in object storage.",
            "format": "uri",
            "type": "string"
          },
          "modelType": {
            "type": "string"
          },
          "sha256": {
            "description": "SHA-256 of the artifact at downloadUrl. Hex, lowercase.",
            "maxLength": 64,
            "minLength": 64,
            "type": "string"
          },
          "sizeBytes": {
            "minimum": 0,
            "type": "integer"
          },
          "version": {
            "description": "Monotonically increasing aggregated model version.",
            "minimum": 0,
            "type": "integer"
          },
          "weights": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/FederatedApi.ModelWeights"
              },
              {
                "items": {
                  "type": "number"
                },
                "type": "array"
              }
            ],
            "description": "Inline weights snapshot for lightweight models. Either a VAS weights object (attention/emotion/body/focus) or a raw numeric vector (used for screen-taste embeddings)."
          }
        },
        "required": [
          "modelType",
          "version"
        ],
        "type": "object"
      },
      "FederatedApi.ModelLatestResponse": {
        "properties": {
          "available": {
            "description": "True when a usable model version exists for the requested slice.",
            "type": "boolean"
          },
          "currentVersion": {
            "description": "Echo of the latest model version (legacy field; alias of model.version).",
            "minimum": 0,
            "type": "integer"
          },
          "model": {
            "$ref": "#/components/schemas/FederatedApi.ModelLatestDescriptor"
          },
          "success": {
            "type": "boolean"
          },
          "updateAvailable": {
            "description": "When the client included a modelVersion query, true if server.version > client.modelVersion.",
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "available",
          "model"
        ],
        "type": "object"
      },
      "FederatedApi.ModelWeights": {
        "properties": {
          "attentionWeight": {
            "type": "number"
          },
          "bodyWeight": {
            "type": "number"
          },
          "emotionWeight": {
            "type": "number"
          },
          "focusWeight": {
            "type": "number"
          }
        },
        "required": [
          "attentionWeight",
          "emotionWeight",
          "bodyWeight",
          "focusWeight"
        ],
        "type": "object"
      },
      "FederatedApi.PerformanceUploadPayload": {
        "additionalProperties": false,
        "properties": {
          "accuracy": {
            "description": "Classifier accuracy on the local validation window. 0–1.",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "anomalyCount": {
            "minimum": 0,
            "type": "integer"
          },
          "deviceId": {
            "maxLength": 64,
            "type": "string"
          },
          "fingerprint": {
            "maxLength": 128,
            "type": "string"
          },
          "latencyMs": {
            "minimum": 0,
            "type": "number"
          },
          "modelType": {
            "maxLength": 64,
            "minLength": 1,
            "type": "string"
          },
          "modelVersion": {
            "minimum": 0,
            "type": "integer"
          },
          "sampleCount": {
            "minimum": 0,
            "type": "integer"
          },
          "screenId": {
            "description": "Screen identifier. May arrive via X-Device-Fingerprint header instead.",
            "maxLength": 64,
            "minLength": 8,
            "type": "string"
          },
          "variant": {
            "maxLength": 32,
            "type": "string"
          },
          "venueType": {
            "description": "Venue category for slice scoping of the performance report.",
            "maxLength": 64,
            "type": "string"
          }
        },
        "required": [
          "modelType",
          "venueType"
        ],
        "type": "object"
      },
      "FederatedApi.PerformanceUploadResponse": {
        "properties": {
          "recorded": {
            "type": "boolean"
          },
          "success": {
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "recorded"
        ],
        "type": "object"
      },
      "FederatedApi.TasteUploadPayload": {
        "additionalProperties": false,
        "properties": {
          "daypart": {
            "maxLength": 32,
            "type": "string"
          },
          "deviceProfile": {
            "maxLength": 64,
            "type": "string"
          },
          "fingerprint": {
            "description": "Device-stable fingerprint for attestation binding.",
            "maxLength": 128,
            "type": "string"
          },
          "geo": {
            "maxLength": 32,
            "type": "string"
          },
          "publicKeyId": {
            "description": "Ed25519 public-key identifier for the signing device.",
            "maxLength": 128,
            "type": "string"
          },
          "screenId": {
            "description": "Bound screen identifier. Required for taste-slice routing.",
            "maxLength": 64,
            "minLength": 8,
            "type": "string"
          },
          "screenMongoId": {
            "description": "MongoDB ObjectId for the screen (legacy compatibility).",
            "maxLength": 64,
            "type": "string"
          },
          "signature": {
            "description": "Base64-encoded Ed25519 signature over the canonical taste payload.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "tasteEmbedding": {
            "description": "Fixed-length screen-taste embedding vector. Length must equal 64.",
            "items": {
              "type": "number"
            },
            "type": "array"
          },
          "tasteSampleCount": {
            "description": "Number of content-impression samples that contributed to the embedding.",
            "minimum": 1,
            "type": "integer"
          },
          "tasteVersionHash": {
            "description": "Stable hash of the taste-model version that produced the embedding.",
            "maxLength": 64,
            "minLength": 8,
            "type": "string"
          },
          "timestamp": {
            "description": "Unix-ms timestamp captured when the signature was produced.",
            "minimum": 0,
            "type": "integer"
          },
          "venueType": {
            "maxLength": 64,
            "type": "string"
          }
        },
        "required": [
          "screenId",
          "screenMongoId",
          "tasteEmbedding",
          "tasteVersionHash",
          "tasteSampleCount",
          "fingerprint",
          "publicKeyId",
          "signature",
          "timestamp"
        ],
        "type": "object"
      },
      "FederatedApi.TasteUploadResponse": {
        "properties": {
          "accepted": {
            "type": "boolean"
          },
          "queuedAt": {
            "description": "RFC3339 timestamp when the embedding was admitted to the aggregation buffer.",
            "format": "date-time",
            "type": "string"
          },
          "sliceKey": {
            "description": "Canonical taste-slice routing key.",
            "type": "string"
          },
          "success": {
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "accepted",
          "sliceKey",
          "queuedAt"
        ],
        "type": "object"
      },
      "FeedbackBrand": {
        "properties": {
          "advertisement_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "brand_name": {
            "type": "string"
          },
          "profile_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "reactive_enabled": {
            "enum": [
              true
            ],
            "type": "boolean"
          },
          "stance": {
            "enum": [
              "adversarial",
              "complementary",
              "same_brand",
              "intent_capture"
            ],
            "type": "string"
          }
        },
        "required": [
          "profile_id",
          "brand_name",
          "advertisement_id",
          "reactive_enabled",
          "stance"
        ],
        "type": "object"
      },
      "FeedbackBrandRequest": {
        "additionalProperties": false,
        "properties": {
          "brand_assets": {
            "default": [],
            "description": "Native brand reference assets used by creative generation.",
            "items": {
              "additionalProperties": {},
              "type": "object"
            },
            "type": "array"
          },
          "brand_intent_text": {
            "description": "Optional brand intent supplied to the native creative generator.",
            "nullable": true,
            "type": "string"
          },
          "brand_name": {
            "description": "Advertiser brand name for the experiment’s native campaign.",
            "minLength": 1,
            "type": "string"
          },
          "brand_rules": {
            "description": "Native brand colours, typography, voice, products and required or prohibited elements.",
            "properties": {
              "brand_name": {
                "description": "The advertiser brand name, e.g. \"Nike\".",
                "type": "string"
              },
              "mandatory_elements": {
                "description": "Elements that MUST appear in every creative, e.g. [\"logo top-right\",\"legal disclaimer\"]; may be empty.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "palette": {
                "description": "Brand colour palette.",
                "properties": {
                  "accents": {
                    "description": "Accent colours; may be empty.",
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "primary": {
                    "description": "Primary brand colour (hex or named); empty string if unknown.",
                    "type": "string"
                  },
                  "secondary": {
                    "description": "Secondary brand colour; empty string if none.",
                    "type": "string"
                  }
                },
                "required": [
                  "primary",
                  "secondary",
                  "accents"
                ],
                "type": "object"
              },
              "products": {
                "description": "Key products to feature; may be empty.",
                "items": {
                  "properties": {
                    "description": {
                      "description": "Short product description; empty string if none.",
                      "type": "string"
                    },
                    "name": {
                      "description": "Product name.",
                      "type": "string"
                    }
                  },
                  "required": [
                    "name",
                    "description"
                  ],
                  "type": "object"
                },
                "type": "array"
              },
              "prohibited_elements": {
                "description": "Elements that must NEVER appear, e.g. [\"competitor logos\",\"alcohol\"]; may be empty.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "stance_preferences": {
                "description": "Preferred reactive stances for this brand; may be empty.",
                "items": {
                  "enum": [
                    "adversarial",
                    "complementary",
                    "same_brand",
                    "intent_capture"
                  ],
                  "type": "string"
                },
                "type": "array"
              },
              "taglines": {
                "description": "Approved taglines / slogans; may be empty.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "typography": {
                "description": "Typography guidance.",
                "properties": {
                  "body": {
                    "description": "Body typeface / style guidance; empty string if none.",
                    "type": "string"
                  },
                  "headline": {
                    "description": "Headline typeface / style guidance; empty string if none.",
                    "type": "string"
                  }
                },
                "required": [
                  "headline",
                  "body"
                ],
                "type": "object"
              },
              "voice_tone": {
                "description": "Voice + tone adjectives, e.g. [\"confident\",\"playful\"]; may be empty.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "required": [
              "brand_name",
              "palette",
              "typography",
              "voice_tone",
              "mandatory_elements",
              "prohibited_elements",
              "products",
              "taglines",
              "stance_preferences"
            ],
            "type": "object"
          },
          "profile_id": {
            "description": "Optional existing brand profile owned by this advertiser and advertisement.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "stance": {
            "default": "intent_capture",
            "description": "Native reactive relationship used to generate or stage this creative.",
            "enum": [
              "adversarial",
              "complementary",
              "same_brand",
              "intent_capture"
            ],
            "type": "string"
          }
        },
        "required": [
          "brand_name",
          "brand_rules"
        ],
        "type": "object"
      },
      "FeedbackBrandResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FeedbackBrand"
          },
          "success": {
            "enum": [
              true
            ],
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "data"
        ],
        "type": "object"
      },
      "FeedbackCreateRequest": {
        "additionalProperties": false,
        "properties": {
          "expected_policy_version": {
            "default": "0",
            "description": "Exact decimal policy version; zero enrolls an unclaimed native policy.",
            "maxLength": 19,
            "pattern": "^(0|[1-9][0-9]*)$",
            "type": "string"
          },
          "idempotency_key": {
            "description": "Caller-owned key for this exact operation; do not reuse for changed inputs.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "media_buy_id": {
            "description": "Your funded native AdCP media buy ID.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "description": "Name for this funded creative feedback experiment.",
            "maxLength": 200,
            "minLength": 1,
            "type": "string"
          },
          "objective": {
            "description": "Measured objective: gaze share 0–1, gaze overlap milliseconds, or capped mean dwell milliseconds.",
            "enum": [
              "gaze_share",
              "gaze_overlap_ms",
              "dwell_ms"
            ],
            "type": "string"
          },
          "package_id": {
            "description": "Native package ID within that media buy.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "name",
          "media_buy_id",
          "package_id",
          "objective",
          "idempotency_key"
        ],
        "type": "object"
      },
      "FeedbackCreativeReview": {
        "properties": {
          "success": {
            "type": "boolean"
          }
        },
        "required": [
          "success"
        ],
        "type": "object"
      },
      "FeedbackExperiment": {
        "properties": {
          "advertisement_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "advertiser_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "media_buy_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "objective": {
            "enum": [
              "gaze_share",
              "gaze_overlap_ms",
              "dwell_ms"
            ],
            "type": "string"
          },
          "package_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "policy_mode": {
            "enum": [
              "attribution",
              "experiment",
              "external_agent"
            ],
            "type": "string"
          },
          "policy_version": {
            "maxLength": 19,
            "pattern": "^(0|[1-9][0-9]*)$",
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "variants": {
            "items": {
              "$ref": "#/components/schemas/FeedbackVariant"
            },
            "type": "array"
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "media_buy_id",
          "package_id",
          "advertisement_id",
          "advertiser_id",
          "objective",
          "policy_version",
          "policy_mode",
          "variants"
        ],
        "type": "object"
      },
      "FeedbackExperimentDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/FeedbackExperiment"
          },
          {
            "properties": {
              "creatives": {
                "items": {
                  "$ref": "#/components/schemas/FeedbackRecoveredCreative"
                },
                "type": "array"
              },
              "creatives_truncated": {
                "type": "boolean"
              }
            },
            "required": [
              "creatives",
              "creatives_truncated"
            ],
            "type": "object"
          }
        ]
      },
      "FeedbackExperimentDetailResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FeedbackExperimentDetail"
          },
          "success": {
            "enum": [
              true
            ],
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "data"
        ],
        "type": "object"
      },
      "FeedbackExperimentResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FeedbackExperiment"
          },
          "success": {
            "enum": [
              true
            ],
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "data"
        ],
        "type": "object"
      },
      "FeedbackGenerateRequest": {
        "additionalProperties": false,
        "properties": {
          "aspectRatio": {
            "default": "16:9",
            "description": "Image aspect ratio, such as 16:9, matched to your booked screens.",
            "maxLength": 32,
            "minLength": 1,
            "type": "string"
          },
          "brief": {
            "default": "",
            "description": "Creative direction added to your configured native brand rules.",
            "maxLength": 10000,
            "type": "string"
          },
          "idempotency_key": {
            "description": "Unique caller-owned generation key; committed retries expose creativeId and renderKey in the receipt.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "stance": {
            "default": "intent_capture",
            "description": "Native reactive relationship used to generate or stage this creative.",
            "enum": [
              "adversarial",
              "complementary",
              "same_brand",
              "intent_capture"
            ],
            "type": "string"
          }
        },
        "required": [
          "idempotency_key"
        ],
        "type": "object"
      },
      "FeedbackGenerateResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FeedbackGeneratedCreative"
          },
          "success": {
            "enum": [
              true
            ],
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "data"
        ],
        "type": "object"
      },
      "FeedbackGeneratedCreative": {
        "properties": {
          "brandReviewStatus": {
            "type": "string"
          },
          "cdnUrl": {
            "type": "string"
          },
          "creativeId": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "deduped": {
            "type": "boolean"
          },
          "headline": {
            "nullable": true,
            "type": "string"
          },
          "moderationStatus": {
            "type": "string"
          },
          "renderKey": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "success": {
            "enum": [
              true
            ],
            "type": "boolean"
          },
          "usage_receipt": {
            "additionalProperties": {},
            "type": "object"
          }
        },
        "required": [
          "success",
          "creativeId",
          "cdnUrl",
          "brandReviewStatus",
          "deduped",
          "renderKey",
          "usage_receipt"
        ],
        "type": "object"
      },
      "FeedbackImportRequest": {
        "additionalProperties": false,
        "description": "Exactly one native image source is required. source_creative_id resolves only within the authenticated partner. Inline bytes require content_type and are validated against the actual image MIME and byte hash by native staging.",
        "properties": {
          "asset_url": {
            "description": "Public HTTPS image URL; provide exactly one image source.",
            "format": "uri",
            "type": "string"
          },
          "content_type": {
            "description": "Actual image MIME; required for inline bytes and checked against decoded content.",
            "enum": [
              "image/png",
              "image/jpeg",
              "image/jpg",
              "image/webp"
            ],
            "type": "string"
          },
          "headline": {
            "description": "Optional headline stored with the native imported creative.",
            "type": "string"
          },
          "idempotency_key": {
            "description": "Unique caller-owned import key; a committed retry returns the existing usage receipt.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "image_base64": {
            "description": "Base64 PNG, JPEG or WebP bytes; provide exactly one source and content_type.",
            "minLength": 1,
            "type": "string"
          },
          "source_creative_id": {
            "description": "Existing creative ID owned by the authenticated partner; provide exactly one image source.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "stance": {
            "default": "intent_capture",
            "description": "Native reactive relationship used to generate or stage this creative.",
            "enum": [
              "adversarial",
              "complementary",
              "same_brand",
              "intent_capture"
            ],
            "type": "string"
          }
        },
        "required": [
          "idempotency_key"
        ],
        "type": "object"
      },
      "FeedbackPolicyRequest": {
        "additionalProperties": false,
        "properties": {
          "expected_version": {
            "description": "Exact decimal policy version from the latest experiment; stale updates return 409.",
            "maxLength": 19,
            "pattern": "^(0|[1-9][0-9]*)$",
            "type": "string"
          },
          "weights": {
            "description": "Replacement weights for registered variants; omitted variants are inactive.",
            "items": {
              "additionalProperties": false,
              "properties": {
                "creative_id": {
                  "description": "Registered native creative ID to assign this weight.",
                  "maxLength": 256,
                  "minLength": 1,
                  "type": "string"
                },
                "weight": {
                  "description": "Weight between zero and one; zero disables selection. Eligible weights normalize at serving.",
                  "maximum": 1,
                  "minimum": 0,
                  "type": "number"
                }
              },
              "required": [
                "creative_id",
                "weight"
              ],
              "type": "object"
            },
            "maxItems": 100,
            "minItems": 1,
            "type": "array"
          }
        },
        "required": [
          "expected_version",
          "weights"
        ],
        "type": "object"
      },
      "FeedbackRecoveredCreative": {
        "properties": {
          "advertisement_mongo_id": {
            "nullable": true,
            "type": "string"
          },
          "brand_profile_id": {
            "nullable": true,
            "type": "string"
          },
          "brand_review_status": {
            "type": "string"
          },
          "cdn_url": {
            "type": "string"
          },
          "content_sha256": {
            "nullable": true,
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "creative_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "headline": {
            "nullable": true,
            "type": "string"
          },
          "linked_to_experiment_ad": {
            "type": "boolean"
          },
          "moderation_status": {
            "nullable": true,
            "type": "string"
          },
          "render_key": {
            "nullable": true,
            "type": "string"
          },
          "reviewed_at": {
            "nullable": true,
            "type": "string"
          },
          "stance": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "creative_id",
          "advertisement_mongo_id",
          "brand_profile_id",
          "cdn_url",
          "headline",
          "stance",
          "render_key",
          "brand_review_status",
          "created_at",
          "reviewed_at",
          "moderation_status",
          "content_sha256",
          "linked_to_experiment_ad"
        ],
        "type": "object"
      },
      "FeedbackRegisterVariantsRequest": {
        "additionalProperties": false,
        "properties": {
          "expected_version": {
            "description": "Exact decimal policy version from the latest experiment; stale updates return 409.",
            "maxLength": 19,
            "pattern": "^(0|[1-9][0-9]*)$",
            "type": "string"
          },
          "variants": {
            "description": "Approved native variants; identity and bytes remain immutable.",
            "items": {
              "additionalProperties": false,
              "properties": {
                "creative_id": {
                  "description": "Approved native generated or imported creative ID.",
                  "maxLength": 256,
                  "minLength": 1,
                  "type": "string"
                },
                "model_version": {
                  "description": "Your model version associated with this immutable variant.",
                  "maxLength": 256,
                  "minLength": 1,
                  "type": "string"
                },
                "variant_id": {
                  "description": "Stable model variant identity within this experiment.",
                  "maxLength": 256,
                  "minLength": 1,
                  "type": "string"
                }
              },
              "required": [
                "variant_id",
                "creative_id"
              ],
              "type": "object"
            },
            "maxItems": 100,
            "minItems": 1,
            "type": "array"
          }
        },
        "required": [
          "expected_version",
          "variants"
        ],
        "type": "object"
      },
      "FeedbackReviewRequest": {
        "additionalProperties": false,
        "properties": {
          "creative_id": {
            "description": "Owned native creative ID awaiting your explicit brand review.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "decision": {
            "description": "Approve or reject brand use; global AI moderation remains a separate gate.",
            "enum": [
              "approve",
              "reject"
            ],
            "type": "string"
          }
        },
        "required": [
          "creative_id",
          "decision"
        ],
        "type": "object"
      },
      "FeedbackReviewResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FeedbackCreativeReview"
          },
          "success": {
            "enum": [
              true
            ],
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "data"
        ],
        "type": "object"
      },
      "FeedbackRewardQuery": {
        "additionalProperties": false,
        "properties": {
          "cursor": {
            "description": "Opaque next_cursor from this same date scope; rewind after scan_complete.",
            "maxLength": 8192,
            "minLength": 1,
            "type": "string"
          },
          "from": {
            "description": "Inclusive first play date, YYYY-MM-DD; date range is at most 31 days.",
            "example": "2026-10-14",
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "type": "string"
          },
          "idempotency_key": {
            "description": "Unique caller-owned billing key for this page; committed retries return the receipt, not another read.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "limit": {
            "description": "Maximum native play states returned on this metered page.",
            "maximum": 5000,
            "minimum": 1,
            "type": "integer"
          },
          "to": {
            "description": "Inclusive last play date, YYYY-MM-DD; date range is at most 31 days.",
            "example": "2026-10-14",
            "format": "date",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "type": "string"
          }
        },
        "required": [
          "from",
          "to",
          "idempotency_key"
        ],
        "type": "object"
      },
      "FeedbackRewardRow": {
        "properties": {
          "action": {
            "enum": [
              "upsert",
              "retract"
            ],
            "type": "string"
          },
          "ad_id": {
            "nullable": true,
            "type": "string"
          },
          "build_stamp": {
            "type": "string"
          },
          "builder_version": {
            "type": "string"
          },
          "canonical_key": {
            "description": "Four ordered strings: play_date, screen_mongo_id, play_source, play_key.",
            "items": {
              "type": "string"
            },
            "maxItems": 4,
            "minItems": 4,
            "type": "array"
          },
          "content_sha256": {
            "type": "string"
          },
          "creative_feedback": {
            "description": "Lossless issued v1 JSON snapshot; empty string when unissued. Contains variant_id and creative_policy_version.",
            "type": "string"
          },
          "creative_id": {
            "nullable": true,
            "type": "string"
          },
          "dwell_capped_ms_mean": {
            "nullable": true,
            "type": "number"
          },
          "dwell_overlap_ms": {
            "nullable": true,
            "type": "number"
          },
          "dwell_windows_n": {
            "nullable": true,
            "type": "number"
          },
          "gaze_face_rows_n": {
            "nullable": true,
            "type": "number"
          },
          "gaze_overlap_ms": {
            "nullable": true,
            "type": "number"
          },
          "gaze_pct_on_screen": {
            "description": "Measured gaze share, 0–1; NULL when not measured.",
            "nullable": true,
            "type": "number"
          },
          "gaze_seconds": {
            "nullable": true,
            "type": "number"
          },
          "gaze_windows_n": {
            "nullable": true,
            "type": "number"
          },
          "play_completed_at": {
            "nullable": true,
            "type": "string"
          },
          "play_date": {
            "type": "string"
          },
          "play_key": {
            "type": "string"
          },
          "play_source": {
            "type": "string"
          },
          "play_started_at": {
            "nullable": true,
            "type": "string"
          },
          "request_id": {
            "nullable": true,
            "type": "string"
          },
          "reward_revision_id": {
            "type": "string"
          },
          "screen_mongo_id": {
            "type": "string"
          },
          "superseded": {
            "type": "number"
          }
        },
        "required": [
          "play_date",
          "screen_mongo_id",
          "play_source",
          "play_key",
          "ad_id",
          "creative_id",
          "request_id",
          "play_started_at",
          "play_completed_at",
          "gaze_pct_on_screen",
          "gaze_overlap_ms",
          "gaze_windows_n",
          "gaze_seconds",
          "gaze_face_rows_n",
          "dwell_capped_ms_mean",
          "dwell_overlap_ms",
          "dwell_windows_n",
          "build_stamp",
          "builder_version",
          "content_sha256",
          "creative_feedback",
          "superseded",
          "canonical_key",
          "reward_revision_id",
          "action"
        ],
        "type": "object"
      },
      "FeedbackRewards": {
        "properties": {
          "experiment_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "next_cursor": {
            "nullable": true,
            "type": "string"
          },
          "objective": {
            "enum": [
              "gaze_share",
              "gaze_overlap_ms",
              "dwell_ms"
            ],
            "type": "string"
          },
          "rewind_next_poll": {
            "type": "boolean"
          },
          "rows": {
            "items": {
              "$ref": "#/components/schemas/FeedbackRewardRow"
            },
            "type": "array"
          },
          "scan_complete": {
            "type": "boolean"
          },
          "usage_receipt": {
            "additionalProperties": {},
            "type": "object"
          },
          "view": {
            "enum": [
              "latest_state"
            ],
            "type": "string"
          }
        },
        "required": [
          "rows",
          "next_cursor",
          "scan_complete",
          "rewind_next_poll",
          "view",
          "experiment_id",
          "objective",
          "usage_receipt"
        ],
        "type": "object"
      },
      "FeedbackRewardsResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FeedbackRewards"
          },
          "success": {
            "enum": [
              true
            ],
            "type": "boolean"
          }
        },
        "required": [
          "success",
          "data"
        ],
        "type": "object"
      },
      "FeedbackStopRequest": {
        "additionalProperties": false,
        "properties": {
          "expected_version": {
            "description": "Exact decimal policy version from the latest experiment; stale updates return 409.",
            "maxLength": 19,
            "pattern": "^(0|[1-9][0-9]*)$",
            "type": "string"
          }
        },
        "required": [
          "expected_version"
        ],
        "type": "object"
      },
      "FeedbackVariant": {
        "properties": {
          "content_sha256": {
            "nullable": true,
            "type": "string"
          },
          "creative_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "model_version": {
            "nullable": true,
            "type": "string"
          },
          "variant_id": {
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "variant_id",
          "creative_id"
        ],
        "type": "object"
      },
      "FleetCommand": {
        "description": "A fleet-command job: one batch that fanned a device command across a screen group's\nmembers. Poll via GET /fleet-commands/{id}.\n",
        "properties": {
          "command_type": {
            "description": "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.",
            "type": "string"
          },
          "completed_at": {
            "description": "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.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "created_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "error": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "failed": {
            "description": "Screens that could not be enqueued",
            "type": "integer"
          },
          "group_id": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "description": "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.",
            "example": "fc_9f8e7d6c-4b1a-4a7e-8c05-2d6e3f9a1b47",
            "type": "string"
          },
          "object": {
            "description": "Constant string `fleet_command` — the Stripe-style type discriminator, present both here and inside the `data` of a `fleet_command.*` webhook.",
            "example": "fleet_command",
            "type": "string"
          },
          "queued": {
            "description": "Screens the command was enqueued to",
            "type": "integer"
          },
          "results": {
            "description": "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.",
            "items": {
              "properties": {
                "device_command_id": {
                  "nullable": true,
                  "type": "string"
                },
                "error": {
                  "nullable": true,
                  "type": "string"
                },
                "screen_id": {
                  "type": "string"
                },
                "status": {
                  "enum": [
                    "queued",
                    "failed"
                  ],
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "status": {
            "description": "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.",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed"
            ],
            "type": "string"
          },
          "targeted": {
            "description": "Member screens resolved for this command",
            "type": "integer"
          },
          "updated_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "FleetCommandEvent": {
        "description": "Webhook body for a group fleet-command terminal state (`fleet_command.completed` |\n`fleet_command.failed`). Standard `{ id, event, timestamp, data }` envelope; `data` is the\n`fleet_command` job object (status, targeted/queued/failed counts, per-screen results; on\nfailure, an `error` message). The event type is also in the `X-Trillboards-Event` header —\nverify the compound `X-Trillboards-Signature` over the raw body first.\n",
        "example": {
          "data": {
            "command_type": "reboot",
            "error": null,
            "failed": 0,
            "group_id": "sg_1a2b3c4d",
            "id": "fc_9z8y7x",
            "object": "fleet_command",
            "queued": 12,
            "status": "completed",
            "targeted": 12
          },
          "event": "fleet_command.completed",
          "id": "evt_lz9k2p_a1b2c3d4",
          "timestamp": "2026-07-01T12:34:56.789Z"
        },
        "properties": {
          "data": {
            "description": "The fleet_command job object.",
            "type": "object"
          },
          "event": {
            "description": "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.",
            "enum": [
              "fleet_command.completed",
              "fleet_command.failed"
            ],
            "type": "string"
          },
          "id": {
            "description": "`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.",
            "example": "evt_lz9k2p_a1b2c3d4",
            "type": "string"
          },
          "timestamp": {
            "description": "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`.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "HeaderBiddingSettings": {
        "description": "Programmatic header-bidding configuration and the resolved VAST waterfall for the screen. The `vast_waterfall.sources[]` array is the programmatic demand path — fetch each source's `vast_url` in priority order and hand the returned VAST to your IMA SDK. Source names are opaque public aliases. Source-health request-pressure governance does not remove SQL/Admin-enabled sources from this catalog.\n",
        "nullable": true,
        "properties": {
          "ad_buffering": {
            "description": "`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.",
            "nullable": true,
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "max_buffer": {
                "type": "number"
              },
              "max_cache_age_seconds": {
                "type": "integer"
              },
              "min_buffer": {
                "type": "number"
              },
              "warmup_count": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "ad_pod": {
            "additionalProperties": true,
            "description": "`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.",
            "nullable": true,
            "type": "object"
          },
          "allocation": {
            "description": "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.",
            "type": "number"
          },
          "auction_request_id": {
            "description": "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.",
            "type": "string"
          },
          "enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "experiment_variant": {
            "description": "`+`-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.",
            "nullable": true,
            "type": "string"
          },
          "fast_loop": {
            "additionalProperties": true,
            "description": "`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.",
            "nullable": true,
            "type": "object"
          },
          "fetch_mode": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "force_request": {
            "description": "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.",
            "type": "boolean"
          },
          "ima_profile": {
            "additionalProperties": true,
            "description": "`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.",
            "nullable": true,
            "type": "object"
          },
          "min_interval_seconds": {
            "description": "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.",
            "type": "integer"
          },
          "prebid": {
            "description": "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.",
            "nullable": true,
            "type": "object"
          },
          "provider": {
            "description": "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.",
            "type": "string"
          },
          "regional_config": {
            "description": "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.",
            "nullable": true,
            "properties": {
              "ad_buffer_percentage": {
                "type": "number"
              },
              "enable_network_adaptation": {
                "type": "boolean"
              },
              "fallback_timeout_ms": {
                "type": "integer"
              },
              "preferred_resolution": {
                "type": "string"
              },
              "timeout_ms": {
                "type": "integer"
              },
              "vquality": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "request_id": {
            "description": "Per-request waterfall identifier; echo it back on programmatic-event reports.",
            "type": "string"
          },
          "request_mode": {
            "description": "`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).",
            "type": "string"
          },
          "source_name": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "source_priority": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "variant_name": {
            "description": "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.",
            "type": "string"
          },
          "vast_cache": {
            "description": "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.",
            "nullable": true,
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "priority": {
                "type": "string"
              },
              "ttl_seconds": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "vast_tag_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          },
          "vast_waterfall": {
            "$ref": "#/components/schemas/VastWaterfall"
          }
        },
        "type": "object"
      },
      "HttpProbeResult": {
        "additionalProperties": true,
        "properties": {
          "host": {
            "description": "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.",
            "type": "string"
          },
          "port": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "server": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "host"
        ],
        "type": "object"
      },
      "HybridError": {
        "description": "Transitional shape: the legacy `{ success, message }` wrapper with the canonical error OBJECT under `error`. Emitted by partner routes that adopted the canonical `code`/`type` vocabulary without moving to the `TrillboardsError` envelope.",
        "properties": {
          "error": {
            "properties": {
              "code": {
                "description": "Stable machine-readable error code. Switch on this, never on `message`. The 143 CATALOGUED codes are published as the `ErrorCode` schema and served live by `GET /v1/errors`, which is the authority. This field is NOT limited to them: services emit further codes directly, and `ApiError.notFound(resource)` mints `<resource>_not_found` from an arbitrary resource name. Always handle an unrecognised code by falling back to `type`.",
                "example": "not_found",
                "type": "string",
                "x-error-code-catalog": "#/components/schemas/ErrorCode"
              },
              "param": {
                "nullable": true,
                "type": "string"
              },
              "type": {
                "$ref": "#/components/schemas/ErrorType"
              }
            },
            "type": "object"
          },
          "message": {
            "type": "string"
          },
          "success": {
            "example": false,
            "type": "boolean"
          }
        },
        "type": "object",
        "x-superseded-by": "TrillboardsError"
      },
      "Impression": {
        "description": "A recorded ad impression / play for a partner screen\n(`@trillboards/api-client` `Impression`).\n",
        "properties": {
          "ad_type": {
            "description": "Kind of ad served (e.g. direct, programmatic, self_promo).",
            "type": "string"
          },
          "advertiser_name": {
            "type": "string"
          },
          "cpm": {
            "description": "Effective CPM for the impression, in the partner's currency.",
            "type": "number"
          },
          "device_id": {
            "description": "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.",
            "type": "string"
          },
          "duration_seconds": {
            "description": "Play duration in seconds.",
            "type": "number"
          },
          "earnings": {
            "description": "Earner revenue accrued for the impression.",
            "type": "number"
          },
          "impression_id": {
            "description": "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.",
            "type": "string"
          },
          "played_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "screen_id": {
            "description": "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.",
            "type": "string"
          }
        },
        "required": [
          "impression_id",
          "screen_id",
          "device_id",
          "played_at"
        ],
        "type": "object"
      },
      "LegacyCodedError": {
        "description": "Legacy error shape carrying a SCREAMING_SNAKE `code` alongside the message — emitted by the auth middlewares (`middleware/auth.js`, `middleware/partnerUserAuth.js`). Its `code` is NOT drawn from `ErrorCode`; it is a separate, older vocabulary.",
        "properties": {
          "code": {
            "description": "Legacy SCREAMING_SNAKE code, e.g. NO_TOKEN, TOKEN_EXPIRED, NO_API_KEY.",
            "example": "NO_TOKEN",
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "success": {
            "example": false,
            "type": "boolean"
          }
        },
        "required": [
          "message"
        ],
        "type": "object",
        "x-superseded-by": "TrillboardsError"
      },
      "LegacyError": {
        "description": "Legacy error shape returned by hand-authored routes that predate the canonical envelope (they call `res.status(n).json(...)` directly rather than routing through `globalErrorHandler`). Documented because it is what these endpoints really return; new endpoints must use `TrillboardsError`.",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "success": {
            "example": false,
            "type": "boolean"
          }
        },
        "type": "object",
        "x-superseded-by": "TrillboardsError"
      },
      "MdnsNetworkDevice": {
        "additionalProperties": true,
        "properties": {
          "host": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "instanceName": {
            "description": "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.",
            "type": "string"
          },
          "mdnsModel": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "mdnsSoftwareVersion": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "mdnsVendor": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "port": {
            "description": "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\".",
            "nullable": true,
            "type": "integer"
          },
          "serviceType": {
            "description": "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.",
            "type": "string"
          }
        },
        "required": [
          "serviceType",
          "instanceName"
        ],
        "type": "object"
      },
      "MeasurementMethodology": {
        "description": "Versioned public disclosure of how audience measurement is produced.\nDescribes WHAT is measured and HOW IT IS QUALIFIED — deliberately never\nnames infrastructure or source locations.\n",
        "properties": {
          "canonical_path": {
            "description": "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.",
            "type": "string"
          },
          "capture_stack": {
            "description": "What each screen observes and sends.",
            "type": "object"
          },
          "cross_day_matching": {
            "description": "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.",
            "type": "object"
          },
          "display_exposure_zone": {
            "description": "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.",
            "type": "object"
          },
          "dwell_qualification_thresholds": {
            "description": "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.",
            "type": "object"
          },
          "edge_confidence_model": {
            "description": "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.",
            "type": "object"
          },
          "evidence_grade_ladder": {
            "description": "Evidence grades, which are decisionable, and person-grain resolution.",
            "type": "object"
          },
          "fallback_path": {
            "description": "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.",
            "type": "string"
          },
          "hashing": {
            "description": "How identifiers are keyed, and what erasure means.",
            "type": "object"
          },
          "identifier_inflation": {
            "description": "The counting UNIT. Reach and unique-device counts are counts of distinct canonical BLE keys (identifiers); 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.",
            "type": "object"
          },
          "mrc_ooh_phase2_alignment": {
            "description": "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).",
            "type": "object"
          },
          "parser_versions": {
            "description": "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.",
            "type": "object"
          },
          "retention": {
            "description": "Per-store retention in days in active systems; `basis` states how each is counted and what the encrypted archive keeps.",
            "type": "object"
          },
          "source_tables": {
            "description": "Tables a reviewer re-derives every published count from.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "version": {
            "description": "Bumped on any disclosure change; drives the ETag.",
            "format": "date",
            "type": "string"
          }
        },
        "required": [
          "version",
          "retention",
          "source_tables",
          "evidence_grade_ladder",
          "mrc_ooh_phase2_alignment"
        ],
        "type": "object"
      },
      "OpenrtbRevenueScreenHourly": {
        "description": "Revenue per screen per hour. Aggregates gross revenue from completed impressions. Refreshed from CH via clickhouseRollupSource. PG only.",
        "properties": {
          "actual_gross_revenue": {
            "example": 1.5,
            "type": "number",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml"
          },
          "estimated_gross_revenue": {
            "example": 1.5,
            "type": "number",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml"
          },
          "estimated_impressions": {
            "example": 0,
            "format": "int64",
            "type": "integer",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml",
            "x-units": "count"
          },
          "gross_revenue": {
            "example": 1.5,
            "type": "number",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml"
          },
          "hour_bucket": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml"
          },
          "impressions": {
            "example": 0,
            "format": "int64",
            "type": "integer",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml",
            "x-units": "count"
          },
          "priced_impressions": {
            "example": 0,
            "format": "int64",
            "type": "integer",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml",
            "x-units": "count"
          },
          "screen_mongo_id": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml",
            "x-units": "text"
          },
          "updated_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml"
          }
        },
        "required": [
          "hour_bucket",
          "screen_mongo_id",
          "impressions",
          "priced_impressions",
          "estimated_impressions",
          "actual_gross_revenue",
          "estimated_gross_revenue",
          "gross_revenue",
          "updated_at"
        ],
        "type": "object",
        "x-sor-ref": "docs/schemas/rollups-reporting/openrtb_revenue_screen_hourly.yaml"
      },
      "Pagination": {
        "properties": {
          "has_more": {
            "description": "Whether more items exist",
            "type": "boolean"
          },
          "limit": {
            "description": "Items per page",
            "type": "integer"
          },
          "offset": {
            "description": "Current offset",
            "type": "integer"
          },
          "total": {
            "description": "Total number of items",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Partner": {
        "properties": {
          "integration_tier": {
            "description": "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`.",
            "type": "string"
          },
          "name": {
            "description": "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.",
            "type": "string"
          },
          "partner_id": {
            "description": "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.",
            "type": "string"
          },
          "partner_type": {
            "deprecated": true,
            "description": "Legacy classification retained for existing integrations. New integrations should not branch on this field.",
            "type": "string"
          },
          "revenue_share_percent": {
            "description": "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.",
            "type": "number"
          },
          "sdk_config": {
            "$ref": "#/components/schemas/SDKConfig"
          },
          "slug": {
            "description": "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.",
            "type": "string"
          },
          "stats": {
            "$ref": "#/components/schemas/PartnerStats"
          },
          "status": {
            "description": "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`.",
            "enum": [
              "pending",
              "active",
              "suspended",
              "terminated"
            ],
            "type": "string"
          },
          "stripe_account_status": {
            "description": "Defaults to the literal `pending` when unset. `POST /earnings/payout` refuses any value other than `active`.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerAdDeliveryProfile": {
        "properties": {
          "android_api_level": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "auto_recovery_enabled": {
            "description": "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. Even when true, a recovery command is queued only for a device that acts on commands, meaning one that acknowledged a command (`POST /device/{deviceId}/command/{commandId}/ack`) in the last 7 days, or that was handed none in that time. A device that let a delivered command expire unacknowledged, and acknowledged none, is sent no recovery command until that command is 7 days old.",
            "type": "boolean"
          },
          "ima_integration": {
            "description": "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.",
            "enum": [
              "android_native",
              "html5_webview",
              "disabled",
              "unknown"
            ],
            "type": "string"
          },
          "mode": {
            "description": "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.",
            "enum": [
              "ima_sdk",
              "vast_fallback"
            ],
            "type": "string"
          },
          "notes": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "reason": {
            "description": "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`.",
            "type": "string"
          },
          "webview_chromium_major": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PartnerCreativeWarnings": {
        "description": "Non-fatal advisories about the creative that was just accepted. The ad IS created and will serve — these describe problems only the uploader can fix. Empty when there is nothing to report. The most important one is an over-length video: a spot longer than the 30 second ad slot is cut off when the next ad starts, so it never reaches its completion event and its final seconds are never seen.",
        "example": [
          "This video is 55s long, which is longer than the 30s ad slot. Playback is cut off when the next ad starts, so the spot never reaches its completion event and the back 25s will not be seen. Trim the creative to 30s or less."
        ],
        "items": {
          "type": "string"
        },
        "type": "array"
      },
      "PartnerDefaultStream": {
        "properties": {
          "_id": {
            "description": "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.",
            "type": "string"
          },
          "canva_url": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "createdAt": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "description": {
            "description": "Falls back to `name`, then to `Background content`; the upload and image routes use `Uploaded video content` / `Image carousel content` instead.",
            "type": "string"
          },
          "duration_seconds": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "embed_url": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "generic_iframe_url": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "google_slides_url": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "iframe_config": {
            "description": "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.",
            "nullable": true,
            "type": "object"
          },
          "image_duration": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "images": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "is_active": {
            "description": "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.",
            "type": "boolean"
          },
          "is_currently_playing": {
            "description": "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.",
            "type": "boolean"
          },
          "name": {
            "description": "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)`.",
            "type": "string"
          },
          "partner_id": {
            "description": "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.",
            "type": "string"
          },
          "playback_order": {
            "description": "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.",
            "type": "integer"
          },
          "screenId": {
            "description": "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.",
            "type": "string"
          },
          "social_wall_url": {
            "description": "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.",
            "type": "string"
          },
          "stream_type": {
            "description": "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.",
            "type": "string"
          },
          "twitch_url": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "updatedAt": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "video_url": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "youtube_url": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerDefaultStreamCreate": {
        "properties": {
          "canva_url": {
            "description": "Accepted with no required-field check, same as `google_slides_url` — a `canva` create with this omitted succeeds and renders nothing.",
            "format": "uri",
            "type": "string"
          },
          "description": {
            "description": "Optional. Falls back to `name` and then to `Background content`.",
            "type": "string"
          },
          "duration_seconds": {
            "description": "Optional explicit duration for iframe-based streams",
            "type": "number"
          },
          "embed_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          },
          "generic_iframe_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          },
          "google_slides_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          },
          "iframe_config": {
            "description": "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.",
            "properties": {
              "allow_flags": {
                "type": "string"
              },
              "auto_advance": {
                "type": "boolean"
              },
              "refresh_interval_seconds": {
                "type": "integer"
              },
              "sandbox_flags": {
                "type": "string"
              },
              "slide_delay_ms": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "image_duration": {
            "description": "Seconds per image",
            "type": "integer"
          },
          "images": {
            "description": "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.",
            "items": {
              "format": "uri",
              "type": "string"
            },
            "type": "array"
          },
          "name": {
            "description": "Optional. Falls back to `description` and then to the literal `Default Stream`, so the created stream always has one.",
            "type": "string"
          },
          "social_wall_url": {
            "description": "Accepted with no required-field check — a `social_wall` create with this omitted succeeds and renders nothing.",
            "format": "uri",
            "type": "string"
          },
          "stream_type": {
            "description": "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`.",
            "enum": [
              "local",
              "youtube",
              "uploaded",
              "images",
              "twitch",
              "google_slides",
              "canva",
              "social_wall",
              "generic_iframe"
            ],
            "type": "string"
          },
          "twitch_url": {
            "description": "Required when the resolved `stream_type` is `twitch`; the create is rejected 400 without it.",
            "format": "uri",
            "type": "string"
          },
          "video_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          },
          "youtube_url": {
            "description": "Required when the resolved `stream_type` is `youtube`; the create is rejected 400 without it. Sending it alone is enough — the type is derived.",
            "format": "uri",
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerDefaultStreamUpdate": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PartnerDefaultStreamCreate"
          }
        ]
      },
      "PartnerDeviceBatchRegistrationInput": {
        "description": "Batch device-registration body (`@trillboards/api-client`\n`PartnerDeviceBatchRegistrationInput`) — up to 100 devices with optional\nfleet-wide `defaults`. `defaults` applies to every device in the batch\nexcept the per-device identifier fields (`device_id` / `device_name`).\n",
        "properties": {
          "defaults": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PartnerDeviceRegistrationInput"
              }
            ],
            "description": "Fleet-wide defaults applied to each device except its identifier fields."
          },
          "devices": {
            "description": "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`.",
            "items": {
              "$ref": "#/components/schemas/PartnerDeviceRegistrationInput"
            },
            "type": "array"
          }
        },
        "required": [
          "devices"
        ],
        "type": "object"
      },
      "PartnerDeviceHeartbeatBody": {
        "additionalProperties": true,
        "description": "Heartbeat payload from agent-core / agent-core-lite Android partner\nSDKs. Only includes fields verified populated in production\n`signal_observations` (last 30 days). Stub fields wired in Kotlin but\nnever observed in CH are intentionally excluded; re-add only after\ntelemetry confirms >1% population over 7 days.\n",
        "properties": {
          "advertisingId": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "advertisingIdType": {
            "description": "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.",
            "enum": [
              "gaid",
              "idfa",
              null
            ],
            "nullable": true,
            "type": "string"
          },
          "advertising_identity": {
            "$ref": "#/components/schemas/AdvertisingIdentity"
          },
          "bleDeviceCount": {
            "description": "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.",
            "minimum": 0,
            "type": "integer"
          },
          "capabilities": {
            "$ref": "#/components/schemas/DeviceCapabilityPayload"
          },
          "discoveredNetworkDevices": {
            "description": "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`).",
            "items": {
              "$ref": "#/components/schemas/MdnsNetworkDevice"
            },
            "maxItems": 50,
            "nullable": true,
            "type": "array"
          },
          "extended_identities": {
            "description": "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.",
            "items": {
              "$ref": "#/components/schemas/ExtendedIdentity"
            },
            "maxItems": 16,
            "type": "array"
          },
          "fingerprint": {
            "description": "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.",
            "type": "string"
          },
          "gatewayIpHash": {
            "description": "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`).",
            "nullable": true,
            "type": "string"
          },
          "httpProbes": {
            "description": "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`).",
            "items": {
              "$ref": "#/components/schemas/HttpProbeResult"
            },
            "maxItems": 50,
            "nullable": true,
            "type": "array"
          },
          "limitAdTracking": {
            "description": "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.",
            "nullable": true,
            "type": "boolean"
          },
          "metadata": {
            "additionalProperties": true,
            "description": "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.",
            "type": "object"
          },
          "nearbyBleDevices": {
            "description": "Capped at 50 devices (RSSI-sorted, strongest first).",
            "items": {
              "$ref": "#/components/schemas/BleScanResultData"
            },
            "maxItems": 50,
            "nullable": true,
            "type": "array"
          },
          "nearbyWifiNetworks": {
            "description": "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`).",
            "items": {
              "$ref": "#/components/schemas/WifiScanResult"
            },
            "maxItems": 50,
            "nullable": true,
            "type": "array"
          },
          "networkDeviceCount": {
            "description": "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`).",
            "minimum": 0,
            "type": "integer"
          },
          "schemaVersion": {
            "description": "Heartbeat schema version (1=baseline, 2=nativeSensorSnapshot)",
            "maximum": 255,
            "minimum": 1,
            "type": "integer"
          },
          "screenId": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "skipReasonCounts": {
            "additionalProperties": {
              "additionalProperties": {
                "minimum": 0,
                "type": "integer"
              },
              "type": "object"
            },
            "description": "Per-source skip-reason counter map. Shape `{source: {reason: count}}`.",
            "type": "object"
          },
          "ssdpDevices": {
            "description": "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`).",
            "items": {
              "$ref": "#/components/schemas/SsdpDeviceInfo"
            },
            "maxItems": 50,
            "nullable": true,
            "type": "array"
          },
          "status": {
            "description": "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.",
            "type": "string"
          },
          "wifiBssidHash": {
            "description": "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`).",
            "nullable": true,
            "type": "string"
          },
          "wifiEnvironment": {
            "$ref": "#/components/schemas/WifiEnvironmentSnapshot"
          },
          "wifiNetworkCount": {
            "description": "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`).",
            "minimum": 0,
            "type": "integer"
          },
          "wifiSsidHash": {
            "description": "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`).",
            "nullable": true,
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerDevicePlay": {
        "additionalProperties": false,
        "description": "A play reported by the partner SDK's reportPlay. The PartnerPlay body without screen_id (the screen is the device's binding), plus the Trillboards impression id.",
        "properties": {
          "ad_id": {
            "description": "Optional ad/line identifier; defaults to the sha-keyed creative.",
            "maxLength": 256,
            "type": "string"
          },
          "completed_at": {
            "description": "When the play completed (device clock), within the last 7 days and at most 5 minutes ahead of the server clock.",
            "example": "2026-09-20T12:00:15.000Z",
            "format": "date-time",
            "type": "string"
          },
          "creative_ref": {
            "description": "The classification reference from POST /creatives — `sha256:<hex>` or the bare 64-char content hash.",
            "example": "sha256:6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1",
            "type": "string"
          },
          "duration_seconds": {
            "description": "Play duration in seconds (default 30).",
            "maximum": 86400,
            "minimum": 0,
            "type": "number"
          },
          "impression_id": {
            "description": "The Trillboards impression id, when the host played an ad Trillboards served. The Trillboards record of that play is then kept and this report is not counted twice.",
            "maxLength": 256,
            "minLength": 1,
            "type": "string"
          },
          "play_id": {
            "description": "Your unique id for this play (idempotency key, shared with POST /v1/partner/plays).",
            "example": "play_20260920T120015Z_7f3a",
            "maxLength": 256,
            "type": "string"
          },
          "request_id": {
            "description": "Optional request identifier.",
            "maxLength": 256,
            "type": "string"
          }
        },
        "required": [
          "creative_ref",
          "play_id",
          "completed_at"
        ],
        "type": "object"
      },
      "PartnerDeviceRegistrationInput": {
        "additionalProperties": true,
        "description": "Request body accepted by the single-device registration endpoints\n(`@trillboards/api-client` `PartnerDeviceRegistrationInput`). Additional\nvendor-specific fields are accepted and forwarded to enrichment.\n",
        "properties": {
          "device_id": {
            "description": "Stable partner-owned device identifier.",
            "type": "string"
          },
          "device_name": {
            "description": "Legacy SDK alias for the device identifier, retained for backwards compatibility.",
            "type": "string"
          },
          "device_type": {
            "description": "Mutable partner category stored independently of advertising classification. Defaults to other on a new device; omission on an existing device preserves its value. Unsupported categories return HTTP 400.",
            "enum": [
              "tv",
              "tablet",
              "display",
              "kiosk",
              "other",
              "mobile",
              "connected_tv",
              "digital_signage",
              "vending_machine"
            ],
            "type": "string"
          },
          "display": {
            "additionalProperties": true,
            "description": "Display specification (pixel resolution and/or physical size).",
            "type": "object"
          },
          "display_settings": {
            "$ref": "#/components/schemas/PartnerRegistrationDisplaySettings"
          },
          "location": {
            "additionalProperties": true,
            "description": "Location hints (lat/lng, address, venue type).",
            "type": "object"
          },
          "name": {
            "description": "Friendly name for the device.",
            "type": "string"
          },
          "placement": {
            "additionalProperties": true,
            "description": "Placement hints for the screen within the venue.",
            "type": "object"
          },
          "venue": {
            "additionalProperties": true,
            "description": "Venue taxonomy hints.",
            "type": "object"
          }
        },
        "type": "object"
      },
      "PartnerHeartbeatCommand": {
        "properties": {
          "command": {
            "description": "Command normalized for partner SDK compatibility.",
            "type": "string"
          },
          "id": {
            "description": "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.",
            "type": "string"
          },
          "payload": {
            "additionalProperties": true,
            "description": "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.",
            "type": "object"
          },
          "queued_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerHeartbeatData": {
        "properties": {
          "ad_delivery_profile": {
            "$ref": "#/components/schemas/PartnerAdDeliveryProfile"
          },
          "commands": {
            "description": "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.",
            "items": {
              "$ref": "#/components/schemas/PartnerHeartbeatCommand"
            },
            "type": "array"
          },
          "last_seen_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "status": {
            "description": "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.",
            "enum": [
              "online",
              "offline"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerHeartbeatMeasurementMetadata": {
        "additionalProperties": true,
        "properties": {
          "ageSeconds": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "cachedAt": {
            "description": "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`.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "receivedAt": {
            "description": "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.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "source": {
            "description": "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.",
            "enum": [
              "redis_twin",
              "pg_fallback",
              "unknown"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerHeartbeatQueryData": {
        "additionalProperties": true,
        "properties": {
          "adDeliveryProfile": {
            "$ref": "#/components/schemas/PartnerAdDeliveryProfile"
          },
          "deviceId": {
            "description": "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.",
            "type": "string"
          },
          "lastHeartbeatAt": {
            "description": "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.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "measurementMetadata": {
            "$ref": "#/components/schemas/PartnerHeartbeatMeasurementMetadata"
          },
          "stalenessSeconds": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "telemetry": {
            "$ref": "#/components/schemas/PartnerSafeHeartbeatTelemetry"
          }
        },
        "required": [
          "deviceId"
        ],
        "type": "object"
      },
      "PartnerHeartbeatQueryResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PartnerHeartbeatQueryData"
          },
          "success": {
            "description": "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`.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "PartnerHeartbeatRequest": {
        "description": "Optional heartbeat payload. Nested telemetry objects are preferred; legacy flat keys\nare still accepted for backward compatibility. The registered\n`deviceId` is the binding boundary for both liveness and identity; no\nseparate partner API key is required.\n",
        "properties": {
          "ad": {
            "description": "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.",
            "properties": {
              "last_vast_request_at": {
                "format": "date-time",
                "type": "string"
              },
              "last_vast_response_at": {
                "format": "date-time",
                "type": "string"
              },
              "proof_of_play_enabled": {
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "advertising_identity": {
            "$ref": "#/components/schemas/AdvertisingIdentity"
          },
          "agent_status": {
            "description": "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.",
            "type": "string"
          },
          "agent_version": {
            "description": "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.",
            "type": "string"
          },
          "android_api_level": {
            "description": "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.",
            "type": "integer"
          },
          "brightness": {
            "description": "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.",
            "type": "number"
          },
          "desugaring_enabled": {
            "description": "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`.",
            "type": "boolean"
          },
          "device": {
            "description": "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`.",
            "properties": {
              "android_api_level": {
                "type": "integer"
              },
              "make": {
                "type": "string"
              },
              "model": {
                "type": "string"
              },
              "os": {
                "type": "string"
              },
              "osv": {
                "type": "string"
              },
              "webview_chromium_major": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "extended_identities": {
            "description": "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.",
            "items": {
              "$ref": "#/components/schemas/ExtendedIdentity"
            },
            "maxItems": 16,
            "type": "array"
          },
          "ima_integration": {
            "description": "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.",
            "enum": [
              "android_native",
              "html5_webview",
              "disabled",
              "unknown"
            ],
            "type": "string"
          },
          "ima_sdk_version": {
            "description": "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.",
            "type": "string"
          },
          "ima_support_reason": {
            "description": "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.",
            "type": "string"
          },
          "ima_supported": {
            "description": "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.",
            "type": "boolean"
          },
          "input_source": {
            "description": "Flat alias of `telemetry.inputSource`. Dead on both sides — no producer emits either casing, and the column has no ClickHouse mirror.",
            "type": "string"
          },
          "last_ima_error_code": {
            "description": "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.",
            "type": "integer"
          },
          "last_vast_error_code": {
            "description": "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.",
            "type": "integer"
          },
          "last_vast_request_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "last_vast_response_at": {
            "description": "Timestamp of the last VAST response received. Like `last_vast_request_at`, no agent or SDK emits it and nothing consumes it.",
            "format": "date-time",
            "type": "string"
          },
          "location": {
            "description": "Current GPS fix for a moving screen. Processed only for screens whose\n`mobility_type` is `mobile` or `semi_mobile` (declare it at registration\nor via PATCH /device/{deviceId}), OR for any screen still missing\ncoordinates (a one-time self-heal that also kicks off venue\nclassification). Static screens with valid coordinates ignore it.\nUpdates live geo-targeting, geofencing and analytics. Fully optional —\nomit it on stationary screens.\n",
            "properties": {
              "accuracy_m": {
                "description": "Horizontal accuracy radius in meters, stored as the device reports it.",
                "example": 12.5,
                "type": "number"
              },
              "buffered_locations": {
                "description": "Up to 200 fixes recorded while the device was offline, replayed as a\nhistorical track. Timestamps MUST be in ascending order — a\nnon-chronological batch is rejected with HTTP 400. Only the top-level\n`location` updates the live serving position.\n",
                "items": {
                  "properties": {
                    "accuracy_m": {
                      "type": "number"
                    },
                    "heading": {
                      "type": "number"
                    },
                    "lat": {
                      "format": "double",
                      "type": "number"
                    },
                    "lng": {
                      "format": "double",
                      "type": "number"
                    },
                    "speed_mps": {
                      "type": "number"
                    },
                    "timestamp": {
                      "format": "int64",
                      "type": "integer"
                    }
                  },
                  "required": [
                    "lat",
                    "lng"
                  ],
                  "type": "object"
                },
                "maxItems": 200,
                "type": "array"
              },
              "heading": {
                "description": "Bearing in degrees (0–360, 0 = true north). Omit if unavailable.",
                "type": "number"
              },
              "lat": {
                "description": "Latitude in decimal degrees (WGS 84).",
                "example": 40.7646,
                "format": "double",
                "type": "number"
              },
              "lng": {
                "description": "Longitude in decimal degrees (WGS 84).",
                "example": -73.937,
                "format": "double",
                "type": "number"
              },
              "source": {
                "default": "gps",
                "description": "`gps` — a live on-device fix. `gps_buffered` — a fix recorded while\noffline and replayed on reconnect.\n",
                "enum": [
                  "gps",
                  "gps_buffered"
                ],
                "type": "string"
              },
              "speed_mps": {
                "description": "Ground speed in meters per second. Omit if unavailable.",
                "type": "number"
              },
              "timestamp": {
                "description": "Unix epoch milliseconds of the on-device fix. Send the device clock\nvalue. A fix whose timestamp is more than 60 s ahead of the server's\nclock, or more than 6 h behind it, is kept without a fix time. A\nposition jump implying > 320 km/h between consecutive fixes is\nrejected.\n",
                "example": 1718200800000,
                "format": "int64",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "metadata": {
            "additionalProperties": true,
            "description": "Alternate namespace for telemetry keys (legacy support).",
            "type": "object"
          },
          "muted": {
            "description": "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.",
            "type": "boolean"
          },
          "network": {
            "description": "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.",
            "properties": {
              "bandwidthMbps": {
                "type": "number"
              },
              "connectionType": {
                "type": "string"
              },
              "downlinkMbps": {
                "type": "number"
              },
              "effectiveType": {
                "type": "string"
              },
              "latencyRtt": {
                "type": "number"
              },
              "rtt": {
                "type": "number"
              },
              "saveData": {
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "network_strength_dbm": {
            "description": "Flat alias of `telemetry.networkStrengthDbm`, in dBm (negative). No agent or SDK populates either spelling, so it is absent fleet-wide.",
            "type": "number"
          },
          "power_state": {
            "description": "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.",
            "enum": [
              "on",
              "standby",
              "off",
              "unknown"
            ],
            "type": "string"
          },
          "proof_of_play_enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "sdk": {
            "description": "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`.",
            "properties": {
              "android_api_level": {
                "type": "integer"
              },
              "desugaring_enabled": {
                "type": "boolean"
              },
              "ima_integration": {
                "enum": [
                  "android_native",
                  "html5_webview",
                  "disabled",
                  "unknown"
                ],
                "type": "string"
              },
              "ima_sdk_version": {
                "type": "string"
              },
              "ima_supported": {
                "type": "boolean"
              },
              "version": {
                "type": "string"
              },
              "webview_chromium_major": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "sdk_version": {
            "description": "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.",
            "type": "string"
          },
          "telemetry": {
            "$ref": "#/components/schemas/PartnerHeartbeatTelemetry"
          },
          "temperature_c": {
            "description": "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.",
            "type": "number"
          },
          "uptime_seconds": {
            "description": "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`.",
            "type": "number"
          },
          "volume": {
            "description": "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.",
            "type": "number"
          },
          "webview_chromium_major": {
            "description": "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.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PartnerHeartbeatResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/PartnerHeartbeatData"
          },
          "success": {
            "description": "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.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "PartnerHeartbeatTelemetry": {
        "additionalProperties": true,
        "properties": {
          "agentStatus": {
            "description": "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.",
            "type": "string"
          },
          "agentVersion": {
            "description": "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`.",
            "type": "string"
          },
          "android_api_level": {
            "description": "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).",
            "type": "integer"
          },
          "bandwidthMbps": {
            "description": "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.",
            "type": "number"
          },
          "brightness": {
            "description": "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.",
            "type": "number"
          },
          "connectionType": {
            "description": "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.",
            "type": "string"
          },
          "desugaring_enabled": {
            "description": "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`.",
            "type": "boolean"
          },
          "downlinkMbps": {
            "description": "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.",
            "type": "number"
          },
          "ima_integration": {
            "description": "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.",
            "enum": [
              "android_native",
              "html5_webview",
              "disabled",
              "unknown"
            ],
            "type": "string"
          },
          "ima_sdk_version": {
            "description": "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`.",
            "type": "string"
          },
          "ima_support_reason": {
            "description": "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.",
            "type": "string"
          },
          "ima_supported": {
            "description": "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.",
            "type": "boolean"
          },
          "inputSource": {
            "description": "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.",
            "type": "string"
          },
          "last_ima_error_code": {
            "description": "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.",
            "type": "integer"
          },
          "last_vast_error_code": {
            "description": "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.",
            "type": "integer"
          },
          "last_vast_request_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "last_vast_response_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "latencyRtt": {
            "description": "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.",
            "type": "number"
          },
          "make": {
            "description": "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.",
            "type": "string"
          },
          "model": {
            "description": "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.",
            "type": "string"
          },
          "muted": {
            "description": "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.",
            "type": "boolean"
          },
          "networkConnectionType": {
            "description": "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`.",
            "type": "string"
          },
          "networkEffectiveType": {
            "description": "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`.",
            "type": "string"
          },
          "networkStrengthDbm": {
            "description": "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`.",
            "type": "number"
          },
          "os": {
            "description": "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.",
            "type": "string"
          },
          "osVersion": {
            "description": "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.",
            "type": "string"
          },
          "osv": {
            "description": "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`.",
            "type": "string"
          },
          "powerState": {
            "description": "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.",
            "enum": [
              "on",
              "standby",
              "off",
              "unknown"
            ],
            "type": "string"
          },
          "proof_of_play_enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "rtt": {
            "description": "Second-precedence spelling of `latencyRtt` (milliseconds), ignored when `latencyRtt` is also present.",
            "type": "number"
          },
          "saveData": {
            "description": "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`.",
            "type": "boolean"
          },
          "temperatureC": {
            "description": "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.",
            "type": "number"
          },
          "uptimeSeconds": {
            "description": "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.",
            "type": "number"
          },
          "volume": {
            "description": "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.",
            "type": "number"
          },
          "webview_chromium_major": {
            "description": "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.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PartnerInteraction": {
        "additionalProperties": false,
        "description": "What a rider did with the screen, reported by the partner SDK's reportInteraction. (partner, device, interaction_id) is the event's identity.",
        "properties": {
          "creative_id": {
            "description": "The creative on screen, as the host names it.",
            "maxLength": 256,
            "type": "string"
          },
          "interaction_id": {
            "description": "The host app's id for this interaction (idempotency key, per device).",
            "example": "int_5d21",
            "maxLength": 128,
            "type": "string"
          },
          "occurred_at": {
            "description": "When it happened (device clock), within the last 7 days and at most 5 minutes ahead of the server clock.",
            "example": "2026-09-20T12:00:08.000Z",
            "format": "date-time",
            "type": "string"
          },
          "t_ms": {
            "description": "Milliseconds into the creative's playback when it happened.",
            "maximum": 4294967295,
            "minimum": 0,
            "type": "integer"
          },
          "type": {
            "description": "What happened. seatbelt_reminder_shown = the host app showed a SeatbeltReminder that agent-core 1.24 delivered to its safety-event listener; send the reminder's eventId as interaction_id. Never a programmatic ad-lifecycle event (those are POST /device/{deviceId}/programmatic-event).",
            "enum": [
              "tap",
              "qr_shown",
              "qr_scanned",
              "dwell_prompt",
              "seatbelt_reminder_shown"
            ],
            "example": "tap",
            "type": "string"
          }
        },
        "required": [
          "interaction_id",
          "type",
          "occurred_at"
        ],
        "type": "object"
      },
      "PartnerLBarContent": {
        "properties": {
          "business_name": {
            "description": "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.",
            "type": "string"
          },
          "contact_info": {
            "description": "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.",
            "type": "object"
          },
          "current_promotion": {
            "description": "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.",
            "type": "string"
          },
          "custom_messages": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "display_settings": {
            "description": "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.",
            "type": "object"
          },
          "enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "live_updates": {
            "description": "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.",
            "type": "object"
          },
          "social_media": {
            "description": "Holds `facebook`, `instagram`, `twitter` and `google_reviews`, free text and empty-string when unset, shallow-merged on PUT.",
            "type": "object"
          },
          "store_hours": {
            "description": "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.",
            "type": "object"
          }
        },
        "type": "object"
      },
      "PartnerLBarDisplayPreferences": {
        "properties": {
          "ad_surface_mode": {
            "description": "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.",
            "enum": [
              "fullscreen",
              "lbar",
              "pip",
              "all",
              null
            ],
            "nullable": true,
            "type": "string"
          },
          "display_mode": {
            "description": "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.",
            "enum": [
              "fullscreen",
              "l-bar"
            ],
            "type": "string"
          },
          "l_bar_ad_duration": {
            "description": "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.",
            "type": "integer"
          },
          "l_bar_ad_interval": {
            "description": "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.",
            "type": "integer"
          },
          "l_bar_ads_enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "l_bar_position": {
            "description": "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`).",
            "enum": [
              "bottom",
              "right"
            ],
            "type": "string"
          },
          "l_bar_size": {
            "description": "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%.",
            "type": "integer"
          },
          "pip_settings": {
            "description": "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.",
            "properties": {
              "aspect_ratio": {
                "type": "number"
              },
              "enabled": {
                "type": "boolean"
              },
              "margin": {
                "type": "integer"
              },
              "position": {
                "enum": [
                  "top-left",
                  "top-right",
                  "bottom-left",
                  "bottom-right"
                ],
                "type": "string"
              },
              "size_ratio": {
                "type": "number"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "PartnerLBarSettings": {
        "properties": {
          "ad_surfaces": {
            "description": "The resolved renderable ad surfaces, one entry per surface (`fullscreen`, `lbar_slot_1..N`, `pip`). Present ONLY on the PUT response; the GET does not compute it.",
            "items": {
              "properties": {
                "allowed_ad_types": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                },
                "dimensions": {
                  "properties": {
                    "height": {
                      "type": "integer"
                    },
                    "width": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                },
                "display_area_ratio": {
                  "type": "number"
                },
                "display_mode": {
                  "type": "string"
                },
                "enabled": {
                  "type": "boolean"
                },
                "id": {
                  "type": "string"
                },
                "surface_group": {
                  "type": "string"
                },
                "surface_type": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "display_preferences": {
            "$ref": "#/components/schemas/PartnerLBarDisplayPreferences"
          },
          "l_bar_content": {
            "$ref": "#/components/schemas/PartnerLBarContent"
          },
          "playback_policy": {
            "description": "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`.",
            "type": "object"
          }
        },
        "type": "object"
      },
      "PartnerLocationCapability": {
        "description": "Where the screen's location comes from, and, per rail, whether that rail\nhas the location it needs. Location is a readiness capability, never a\nregistration rule: a device without one is registered, and this says\nwhich rails serve it until a position arrives. `monetization_status` is\ncomputed over `inventory_feed` and `geo_targeted_demand`.\n",
        "properties": {
          "position": {
            "description": "`precise` — the screen or its device has coordinates; `fleet_market` — none of its own, your account's declared fleet market location lists it; `none`.",
            "enum": [
              "precise",
              "fleet_market",
              "none"
            ],
            "type": "string"
          },
          "rails": {
            "properties": {
              "geo_targeted_demand": {
                "$ref": "#/components/schemas/PartnerLocationRail"
              },
              "inventory_feed": {
                "$ref": "#/components/schemas/PartnerLocationRail"
              },
              "visit_attribution": {
                "$ref": "#/components/schemas/PartnerLocationRail"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "PartnerLocationRail": {
        "properties": {
          "location_source": {
            "description": "Where a ready rail's location comes from; null when blocked. Only `inventory_feed` reads `fleet_market`.",
            "enum": [
              "screen",
              "device",
              "fleet_market",
              null
            ],
            "nullable": true,
            "type": "string"
          },
          "reason": {
            "description": "Why a blocked rail has no location; null when ready. See https://api.trillboards.com/docs/monetization-requirements#location-capability.",
            "enum": [
              "awaiting_device_gps",
              "no_location_provided",
              "city_without_state",
              "geocode_failed",
              "screen_position_pending",
              "country_unknown",
              "country_not_supported",
              null
            ],
            "nullable": true,
            "type": "string"
          },
          "status": {
            "enum": [
              "ready",
              "blocked"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerMonetizationRequirements": {
        "description": "Stripe-style monetization requirements (account.requirements analogue).\nReturned on device register / update / get / batch so bulk partners can\ndetect and self-remediate missing data instead of discovering silent\ngaps later. `currently_due` blocks or limits monetization now;\n`eventually_due` improves serving but isn't blocking.\n",
        "properties": {
          "currently_due": {
            "description": "Requirements blocking or limiting monetization right now.",
            "items": {
              "$ref": "#/components/schemas/PartnerRequirement"
            },
            "type": "array"
          },
          "eventually_due": {
            "description": "Non-blocking improvements (e.g. display dimensions).",
            "items": {
              "$ref": "#/components/schemas/PartnerRequirement"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "PartnerNetworkConfig": {
        "additionalProperties": false,
        "properties": {
          "ad_delivery": {
            "additionalProperties": false,
            "description": "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`.",
            "properties": {
              "cache_max_age_seconds": {
                "description": "`/ads` response cache lifetime. Zero requires a fresh response for every request.",
                "example": 30,
                "maximum": 300,
                "minimum": 0,
                "type": "integer"
              },
              "identity_mode": {
                "description": "Delegates resettable advertising identity to the native Google IMA SDK; the API never fabricates one. Set null to restore the platform default identity path.",
                "enum": [
                  "ima_sdk"
                ],
                "nullable": true,
                "type": "string"
              },
              "max_ads_per_hour": {
                "description": "Fleet-wide hourly ad cap returned in `settings.max_ads_per_hour`.",
                "example": 30,
                "maximum": 120,
                "minimum": 1,
                "type": "integer"
              },
              "non_personalized_ads": {
                "description": "Requests non-personalized demand when true. Never provide an advertising identifier unless the OS legitimately exposes it.",
                "type": "boolean"
              },
              "request_interval_seconds": {
                "description": "Returned as both `settings.ad_interval` and `header_bidding_settings.min_interval_seconds`; the player should request each monetizable slot at this cadence.",
                "example": 120,
                "maximum": 300,
                "minimum": 30,
                "type": "integer"
              },
              "self_promo_priority_mode": {
                "description": "Controls whether a scheduled self-promo/house slot may lead the\nVAST waterfall. `scheduled_primary` preserves the bounded house\ncadence; `fallback_only` keeps self-promo behind paid\nprogrammatic demand; `disabled` removes the self-promo/house\ncreative from your screens entirely — it is served on neither\n`ads[]` nor the VAST waterfall, and no new house creative is\ngenerated for your screens. Paid-direct guarantees always retain\npriority and are never affected. Set null to restore\n`scheduled_primary`.\n",
                "enum": [
                  "scheduled_primary",
                  "fallback_only",
                  "disabled"
                ],
                "nullable": true,
                "type": "string"
              }
            },
            "type": "object"
          },
          "branding": {
            "additionalProperties": false,
            "description": "White-label presentation for your buyer-facing advertiser portal. These values\nare read and written directly against the branding your portal renders — a GET\nreturns what a buyer currently sees, and a write takes effect on their next page\nload. Omitted entirely if no white-label portal is linked to your partner\naccount; writing branding in that state returns `409`\n`white_label_distributor_not_linked` rather than accepting values that would\nrender nowhere. Contact Trillboards to have one linked.\n",
            "properties": {
              "display_name": {
                "description": "Shown as the portal name and the browser tab title.",
                "example": "Bowling Center TV",
                "type": "string"
              },
              "favicon_url": {
                "description": "Absolute HTTPS URL.",
                "type": "string"
              },
              "font_body": {
                "example": "Roboto",
                "type": "string"
              },
              "font_heading": {
                "example": "Poppins",
                "type": "string"
              },
              "logo_url": {
                "description": "Absolute HTTPS URL. Rendered in the portal header.",
                "type": "string"
              },
              "primary_color": {
                "description": "CSS color. HSL triplets (\"210 90% 40%\") and hex are both accepted.",
                "example": "210 90% 40%",
                "type": "string"
              },
              "secondary_color": {
                "example": "211 100% 99%",
                "type": "string"
              },
              "tagline": {
                "example": "Reach every lane in America",
                "type": "string"
              }
            },
            "type": "object"
          },
          "default_content_preferences": {
            "additionalProperties": true,
            "description": "Default content preferences inherited by newly provisioned screens.",
            "type": "object"
          },
          "default_waterfall": {
            "additionalProperties": false,
            "description": "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.",
            "properties": {
              "source_order": {
                "description": "Preferred opaque source aliases from the `/ads` response. Sources\nnot listed here remain enabled and are appended. Internal demand\nnames and `sort_by: cpm` are rejected.\n",
                "example": [
                  "var_e4f66f",
                  "var_fa068b"
                ],
                "items": {
                  "pattern": "^(src|var)_[0-9a-fA-F]{6}(_[a-zA-Z0-9]{2,16})?$",
                  "type": "string"
                },
                "maxItems": 64,
                "type": "array",
                "uniqueItems": true
              },
              "total_timeout_ms": {
                "description": "Total deadline for one waterfall snapshot, in milliseconds.",
                "example": 15000,
                "maximum": 60000,
                "minimum": 3000,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "storefront": {
            "additionalProperties": false,
            "description": "Limits your branded buyer portal to YOUR OWN screens. Off by default: a branded\nportal otherwise sells the whole Trillboards network, which is what most\nwhite-label partners want.\n\nThe screen set is DERIVED from the media-owner identity your screens already\ncarry — both the ones we host and the ones resold through partner exchanges —\nso it stays correct as you add inventory and there is no list to maintain. It\ncannot be set to another operator's inventory.\n",
            "properties": {
              "enabled": {
                "description": "When true, every inventory surface in your buyers' portal — search, map,\nforecasts, quotes, bookings and proposals — is limited to your own screens.\n",
                "example": true,
                "type": "boolean"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "PartnerNetworkConfigUpdate": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PartnerNetworkConfig"
          }
        ],
        "description": "Partner-safe partial update. Internal `partners`, `supply_chain`, seller\nIDs, direct demand names, and CPM-sort controls are rejected.\n",
        "minProperties": 1
      },
      "PartnerPlay": {
        "description": "A log-level play for a partner-served ad.",
        "properties": {
          "ad_id": {
            "description": "Optional ad/line identifier; defaults to the sha-keyed creative.",
            "type": "string"
          },
          "completed_at": {
            "description": "ISO 8601 timestamp when the play completed.",
            "format": "date-time",
            "type": "string"
          },
          "creative_ref": {
            "description": "The classification reference from POST /creatives — `sha256:<hex>` or the bare 64-char content hash.",
            "example": "sha256:6c4b8f3e2a1d…e1",
            "type": "string"
          },
          "duration_seconds": {
            "description": "Play duration in seconds (default 30).",
            "type": "number"
          },
          "play_id": {
            "description": "Your unique id for this play (idempotency key).",
            "type": "string"
          },
          "request_id": {
            "description": "Optional partner request identifier.",
            "type": "string"
          },
          "screen_id": {
            "description": "One of your registered earner-screen ids.",
            "type": "string"
          }
        },
        "required": [
          "screen_id",
          "creative_ref",
          "play_id",
          "completed_at"
        ],
        "type": "object"
      },
      "PartnerReconciliationReports": {
        "description": "Partner-reported impression and revenue counts for reconciliation against deduped completed_impressions. Ingested from VidVerto SSP API and Adipolo API / JustBaat CSV reports. PG only.",
        "properties": {
          "adjustment_id": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "count"
          },
          "applied_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "applied_report_hash": {
            "description": "applied_report_hash/applied_at (mig 449) gate the hourly pricing re-apply: syncs skip the CI re-stamp + PED rebuild when the applied tuple is unchanged. Written ONLY by partnerPricingApplyGate.reco...",
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "text"
          },
          "created_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "demand_partner_id": {
            "description": "Wave 2 B added demand_partner_id INT + variant_id INT FKs. Legacy partner_key TEXT kept as wire-format alternate-key. Backfill via follow-up Wave 2 B-backfill PR.",
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "count"
          },
          "id": {
            "description": "VidVerto synced daily via vidvertoRevenueSync before programmatic earnings aggregation.",
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "count"
          },
          "imported_by": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "text"
          },
          "impression_variance": {
            "description": "NUMERIC(8,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "our_ecpm": {
            "description": "NUMERIC(10,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "our_impressions": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "count"
          },
          "our_requests": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-04-13T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "count"
          },
          "our_revenue": {
            "description": "NUMERIC(14,6).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "provider_name": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "text"
          },
          "raw_data": {
            "description": "Adipolo publisher revenue requires a completed vast_requests lifecycle plus latest screens_live source='owned'. ID shape/event alone is insufficient; shared, external, and unjoined bundles are excl...",
            "example": {},
            "nullable": true,
            "type": "object",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "report_date": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "report_type": {
            "enum": [
              "partner_api",
              "manual_upload",
              "email_extract",
              "dashboard_snapshot"
            ],
            "example": "partner_api",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "enum"
          },
          "request_variance": {
            "description": "NUMERIC(10,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-04-13T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "resolution_notes": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "text"
          },
          "revenue_variance": {
            "description": "NUMERIC(8,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "reviewed_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "reviewed_by": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "text"
          },
          "status": {
            "enum": [
              "pending",
              "matched",
              "flagged",
              "disputed",
              "resolved",
              "accepted"
            ],
            "example": "pending",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "enum"
          },
          "their_ecpm": {
            "description": "NUMERIC(10,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "their_fill_rate": {
            "description": "NUMERIC(6,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "their_impressions": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "count"
          },
          "their_requests": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-04-13T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "count"
          },
          "their_revenue": {
            "description": "NUMERIC(14,6).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "updated_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
          },
          "variant_id": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml",
            "x-units": "count"
          }
        },
        "required": [
          "provider_name",
          "report_date",
          "report_type",
          "status",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "x-sor-ref": "docs/schemas/ad-lifecycle/partner_reconciliation_reports.yaml"
      },
      "PartnerRegistrationDisplaySettings": {
        "additionalProperties": false,
        "description": "Optional display behavior applied while registering or re-registering a device.",
        "properties": {
          "content_fit": {
            "$ref": "#/components/schemas/ContentFitSettings"
          }
        },
        "type": "object"
      },
      "PartnerRequirement": {
        "properties": {
          "code": {
            "description": "Stable machine code, e.g. location_unresolvable, location_awaiting_device_gps, enrichment_failed, display_size_missing.",
            "example": "location_unresolvable",
            "type": "string"
          },
          "doc_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          },
          "field": {
            "description": "The request field this concerns (location, venue, display).",
            "example": "location",
            "type": "string"
          },
          "message": {
            "description": "Human-readable, actionable remediation.",
            "example": "Only a city (\"New York\") was provided, and a city alone cannot be geocoded. Add state, or send lat/lng or a full street address.",
            "type": "string"
          },
          "reason": {
            "description": "Specific reason variant, e.g. city_without_state, no_location_provided, geocode_failed, awaiting_device_gps.",
            "example": "city_without_state",
            "type": "string"
          }
        },
        "type": "object"
      },
      "PartnerSafeHeartbeatTelemetry": {
        "additionalProperties": true,
        "description": "Partner-safe subset of device telemetry. Strips MDM compliance,\nVertex embeddings, identity-correction internals, partner identifiers,\nand Mongo `_id` so partners only see what they need to decide\nwhether a device is healthy enough to receive ads.\n",
        "properties": {
          "agentStatus": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "agentVersion": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "bandwidthMbps": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "brightness": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "connectionType": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "consecutiveAdFailures": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "effectiveType": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "latencyRtt": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "make": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "model": {
            "description": "Device model, after identity-fusion correction. Like `make`, it is a twin-only field and is absent whenever `measurementMetadata.source` is `pg_fallback`.",
            "nullable": true,
            "type": "string"
          },
          "muted": {
            "description": "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.",
            "nullable": true,
            "type": "boolean"
          },
          "networkStrengthDbm": {
            "description": "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.",
            "maximum": 0,
            "minimum": -120,
            "nullable": true,
            "type": "integer"
          },
          "onlineStatus": {
            "description": "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.",
            "enum": [
              "online",
              "offline",
              "standby",
              "unknown"
            ],
            "nullable": true,
            "type": "string"
          },
          "os": {
            "description": "Operating-system family after correction — the Android agents report the full `\"Android <release>\"` string. Twin-only, so absent on the `pg_fallback` path.",
            "nullable": true,
            "type": "string"
          },
          "osv": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "powerState": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "temperatureC": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "ua": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "uptimeSeconds": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "visibilityState": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "volume": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          }
        },
        "type": "object"
      },
      "PartnerSelfPromoAd": {
        "properties": {
          "_id": {
            "description": "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.",
            "type": "string"
          },
          "campaign_link": {
            "description": "Click-through / QR destination; empty string, not null, when it was never set — the PG read coalesces it the same way.",
            "format": "uri",
            "type": "string"
          },
          "createdAt": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "description": {
            "description": "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.",
            "type": "string"
          },
          "name": {
            "description": "Required at create. It is also copied onto each placement as `advertisement_name`.",
            "type": "string"
          },
          "photos": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "status": {
            "description": "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`.",
            "type": "string"
          },
          "video": {
            "description": "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.",
            "type": "string"
          },
          "video_duration_seconds": {
            "description": "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.",
            "example": 15,
            "nullable": true,
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PartnerSelfPromoCreate": {
        "properties": {
          "campaign_link": {
            "description": "Click-through / QR destination. Stored as an empty string, not null, when omitted.",
            "format": "uri",
            "type": "string"
          },
          "description": {
            "description": "Optional; defaults to `name` rather than staying null.",
            "type": "string"
          },
          "end_date": {
            "description": "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.",
            "format": "date",
            "type": "string"
          },
          "image_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          },
          "name": {
            "description": "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.",
            "type": "string"
          },
          "screen_ids": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "start_date": {
            "description": "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.",
            "format": "date",
            "type": "string"
          },
          "video_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "PartnerSelfPromoPlacement": {
        "properties": {
          "ads": {
            "description": "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.",
            "items": {
              "properties": {
                "advertisement": {
                  "$ref": "#/components/schemas/PartnerSelfPromoAd"
                },
                "allocation_id": {
                  "type": "string"
                },
                "end_date": {
                  "format": "date",
                  "type": "string"
                },
                "start_date": {
                  "format": "date",
                  "type": "string"
                },
                "status": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "screen": {
            "description": "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.",
            "properties": {
              "_id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "PartnerStats": {
        "properties": {
          "active_devices": {
            "description": "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.",
            "type": "integer"
          },
          "last_impression_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "pending_payout_cents": {
            "description": "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.",
            "type": "integer"
          },
          "total_devices": {
            "description": "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.",
            "type": "integer"
          },
          "total_impressions": {
            "description": "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.",
            "type": "integer"
          },
          "total_revenue_cents": {
            "description": "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.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PartnerTripState": {
        "additionalProperties": false,
        "description": "One taxi-meter state change reported by the partner SDK's setTripState. (partner, device, trip_id, event_seq) is the event's identity.",
        "properties": {
          "destination": {
            "description": "The host's destination text or zone.",
            "maxLength": 512,
            "minLength": 1,
            "type": "string"
          },
          "event_seq": {
            "description": "The event's position in the trip. Increases within a trip; a retry re-sends the same value (idempotency key with trip_id, per device).",
            "example": 0,
            "maximum": 4294967295,
            "minimum": 0,
            "type": "integer"
          },
          "fare_cents": {
            "description": "Fare so far, in the smallest currency unit.",
            "maximum": 4294967295,
            "minimum": 0,
            "type": "integer"
          },
          "meter_on": {
            "description": "true while the meter runs (a hired trip).",
            "example": true,
            "type": "boolean"
          },
          "passenger_count": {
            "description": "Riders the host counted.",
            "maximum": 255,
            "minimum": 0,
            "type": "integer"
          },
          "payment_type": {
            "description": "The host's payment type (stored trimmed and lowercased).",
            "maxLength": 64,
            "minLength": 1,
            "type": "string"
          },
          "state_at": {
            "description": "When the meter changed state (device clock), within the last 7 days and at most 5 minutes ahead of the server clock.",
            "example": "2026-09-20T12:00:00.000Z",
            "format": "date-time",
            "type": "string"
          },
          "tip_cents": {
            "description": "Tip, in the smallest currency unit.",
            "maximum": 4294967295,
            "minimum": 0,
            "type": "integer"
          },
          "trip_id": {
            "description": "The host app's trip id. Unique per device, not per partner.",
            "example": "trip_8841",
            "maxLength": 128,
            "type": "string"
          }
        },
        "required": [
          "trip_id",
          "event_seq",
          "state_at",
          "meter_on"
        ],
        "type": "object"
      },
      "PartnerVariants": {
        "description": "Canonical INT-PK identity for partner variants (e.g. adipolo_lower_floor_tablet). Wave 2 of pristine-state refactor. variant_name TEXT remains as the wire-format alternate-key indefinitely.",
        "properties": {
          "archived": {
            "example": false,
            "type": "boolean",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "boolean"
          },
          "created_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml"
          },
          "daily_budget_cap": {
            "description": "NUMERIC(10,2).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml"
          },
          "daily_impression_cap": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "daily_request_cap": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "demand_partner_id": {
            "description": "Backfill is 1:N from demand_partner_config.variant_names[]. Rows with NULL demand_partner_id at apply time are silently skipped — deploy-controller previous-tick guard ensures Wave 2 A1 has populat...",
            "example": 0,
            "format": "int32",
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "display_name": {
            "description": "vast_tag_variants JSONB on demand_partner_config still co-exists during transition. Backfill UPDATE pulls display_name from the JSONB if present (no-op today; forward-compatible).",
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "text"
          },
          "public_alias": {
            "description": "public_alias is the obfuscation alias for partner-API responses (matches partnerAlias.encodeAlias pattern: 'var_' || sha256(variant_name)[0..5]).",
            "example": "sample",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "text"
          },
          "request_pressure_base_interval_seconds": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "request_pressure_completion_rate_floor": {
            "description": "NUMERIC(12,8).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml"
          },
          "request_pressure_confidence_level": {
            "description": "NUMERIC(6,3).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml"
          },
          "request_pressure_defer_seconds": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "request_pressure_enabled": {
            "description": "request_pressure_enabled=false fails open for pressure shaping only. It must not be treated as demand-source enablement.",
            "example": false,
            "nullable": true,
            "type": "boolean",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "boolean"
          },
          "request_pressure_max_multiplier": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "request_pressure_min_pair_requests": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "request_pressure_min_source_requests": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "request_pressure_probe_interval_seconds": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "request_pressure_probe_modulo": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "count"
          },
          "request_pressure_probe_target_per_minute": {
            "description": "NUMERIC(10,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml"
          },
          "updated_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml"
          },
          "variant_id": {
            "description": "feeds_into lists tables that will gain a nullable variant_id INT FK in Wave 2 Stream B. Each is a separate PR — this PR mints the dimension only.",
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "text"
          },
          "variant_name": {
            "description": "variant_name TEXT is the wire-format alternate-key (kept forever) — internal logs and partner integrations rely on it. Never propose dropping variant_name.",
            "example": "sample",
            "type": "string",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml",
            "x-units": "text"
          }
        },
        "required": [
          "demand_partner_id",
          "variant_name",
          "public_alias",
          "archived",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "x-since": "2026-05-01T00:00:00.000Z",
        "x-sor-ref": "docs/schemas/partnerships/partner_variants.yaml"
      },
      "PartnerZoneCpmDaily": {
        "description": "Per-zone per-day CPM rates from partner APIs. Used for revenue estimation and settled partner-truth allocation when partner_reconciliation_reports does not provide a newer manual/API row. PG only.",
        "properties": {
          "created_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml"
          },
          "demand_partner_id": {
            "description": "Wave 2 B added demand_partner_id INT + variant_id INT FKs. Legacy partner_key TEXT kept as wire-format alternate-key. Backfill via follow-up Wave 2 B-backfill PR.",
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "count"
          },
          "ecpm": {
            "description": "NUMERIC(10,4).",
            "example": 1.5,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml"
          },
          "fetched_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml"
          },
          "fill_rate": {
            "description": "NUMERIC(6,4).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml"
          },
          "gross_revenue": {
            "description": "NUMERIC(14,6).",
            "example": 1.5,
            "nullable": true,
            "type": "number",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml"
          },
          "id": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "text"
          },
          "impressions": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "count"
          },
          "partner_key": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "text"
          },
          "period_date": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml"
          },
          "requests": {
            "example": 0,
            "format": "int64",
            "nullable": true,
            "type": "integer",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "count"
          },
          "source": {
            "description": "Daily earnings aggregation may use these rows to settle gross revenue, but completed_impressions remains the event-count source for programmatic_earnings_daily.impressions_count.",
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "text"
          },
          "updated_at": {
            "example": "2026-05-12T00:00:00.000Z",
            "format": "date-time",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml"
          },
          "variant_id": {
            "example": 0,
            "format": "int32",
            "nullable": true,
            "type": "integer",
            "x-since": "2026-05-01T00:00:00.000Z",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "count"
          },
          "zone_id": {
            "example": "sample",
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "text"
          },
          "zone_label": {
            "example": "sample",
            "nullable": true,
            "type": "string",
            "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml",
            "x-units": "text"
          }
        },
        "required": [
          "partner_key",
          "zone_id",
          "period_date",
          "ecpm",
          "source",
          "fetched_at",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "x-sor-ref": "docs/schemas/ad-lifecycle/partner_zone_cpm_daily.yaml"
      },
      "PayoutResponse": {
        "properties": {
          "amount_cents": {
            "description": "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`.",
            "type": "integer"
          },
          "estimated_arrival": {
            "description": "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.",
            "format": "date",
            "type": "string"
          },
          "payout_id": {
            "description": "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.",
            "type": "string"
          },
          "status": {
            "description": "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.",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ],
            "type": "string"
          },
          "stripe_transfer_id": {
            "description": "**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`.",
            "nullable": true,
            "type": "string"
          }
        },
        "type": "object"
      },
      "PlaygroundResult": {
        "properties": {
          "agent_safe": {
            "description": "Whether the operation is safe for autonomous agents.",
            "type": "boolean"
          },
          "cost_tier": {
            "description": "Billing tier: free, metered, or paid.",
            "type": "string"
          },
          "idempotent": {
            "description": "Whether the operation is idempotent.",
            "type": "boolean"
          },
          "object": {
            "description": "Object type identifier.",
            "enum": [
              "playground_result"
            ],
            "type": "string"
          },
          "operation_id": {
            "example": "dataApiListScreens",
            "type": "string"
          },
          "side_effects": {
            "description": "Declared side effects of the operation.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "valid": {
            "description": "Whether the request body passed validation.",
            "type": "boolean"
          },
          "validated_body": {
            "description": "The parsed/coerced body on success, null on failure."
          },
          "validation_errors": {
            "description": "Array of validation issues on failure, null on success.",
            "items": {
              "$ref": "#/components/schemas/PlaygroundValidationError"
            },
            "nullable": true,
            "type": "array"
          },
          "would_call": {
            "description": "The HTTP method and path this operation maps to.",
            "properties": {
              "method": {
                "example": "GET",
                "type": "string"
              },
              "path": {
                "example": "/v1/data/screens",
                "type": "string"
              }
            },
            "required": [
              "method",
              "path"
            ],
            "type": "object"
          }
        },
        "required": [
          "object",
          "operation_id",
          "would_call",
          "valid",
          "validation_errors",
          "side_effects",
          "agent_safe",
          "idempotent",
          "cost_tier"
        ],
        "type": "object"
      },
      "PlaygroundValidationError": {
        "properties": {
          "code": {
            "description": "Zod issue code.",
            "example": "invalid_type",
            "type": "string"
          },
          "message": {
            "description": "Human-readable validation message.",
            "example": "Required",
            "type": "string"
          },
          "path": {
            "description": "Dot-delimited path to the invalid field.",
            "example": "body.name",
            "type": "string"
          }
        },
        "required": [
          "path",
          "message",
          "code"
        ],
        "type": "object"
      },
      "ProgrammaticSettingsInput": {
        "properties": {
          "enabled": {
            "description": "Enable programmatic ads",
            "type": "boolean"
          },
          "floor_price_cpm": {
            "description": "Minimum CPM in dollars",
            "type": "number"
          },
          "google_ima": {
            "description": "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.",
            "properties": {
              "ad_tag_url": {
                "type": "string"
              },
              "enabled": {
                "type": "boolean"
              },
              "max_duration": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "prebid": {
            "description": "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.",
            "properties": {
              "bidders": {
                "items": {
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "params": {
                      "type": "object"
                    }
                  },
                  "type": "object"
                },
                "type": "array"
              },
              "enabled": {
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "priority": {
            "description": "Ad source priority",
            "enum": [
              "direct_first",
              "programmatic_first",
              "highest_bid"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "ProgrammaticSettingsResponse": {
        "properties": {
          "enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "floor_price_cpm": {
            "description": "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.",
            "type": "number"
          },
          "google_ima": {
            "description": "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.",
            "properties": {
              "ad_tag_url": {
                "type": "string"
              },
              "enabled": {
                "type": "boolean"
              },
              "max_duration": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "object": {
            "description": "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.",
            "example": "programmatic_settings",
            "type": "string"
          },
          "prebid": {
            "description": "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.",
            "properties": {
              "bidders": {
                "items": {
                  "type": "object"
                },
                "type": "array"
              },
              "enabled": {
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "priority": {
            "description": "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.",
            "type": "string"
          },
          "request_id": {
            "description": "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.",
            "type": "string"
          },
          "screen_id": {
            "description": "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.",
            "type": "string"
          },
          "stats": {
            "description": "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.",
            "properties": {
              "avg_cpm": {
                "type": "number"
              },
              "fill_rate_percent": {
                "type": "number"
              },
              "impressions_today": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "updated_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "RequestMetadataResponse": {
        "properties": {
          "duration_ms": {
            "example": 42,
            "type": "integer"
          },
          "error": {
            "description": "Present only when the original request resulted in an error.",
            "properties": {
              "code": {
                "example": "screen_not_found",
                "type": "string"
              },
              "type": {
                "example": "invalid_request_error",
                "type": "string"
              }
            },
            "required": [
              "code",
              "type"
            ],
            "type": "object"
          },
          "method": {
            "example": "GET",
            "type": "string"
          },
          "object": {
            "description": "Object type identifier.",
            "enum": [
              "request"
            ],
            "type": "string"
          },
          "path": {
            "example": "/v1/screens",
            "type": "string"
          },
          "request_id": {
            "example": "req_a1b2c3d4e5f6789012345678901234ab",
            "pattern": "^req_[a-f0-9]{32}$",
            "type": "string"
          },
          "status_code": {
            "example": 200,
            "type": "integer"
          },
          "timestamp": {
            "example": "2026-04-16T12:00:00.000Z",
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "object",
          "request_id",
          "method",
          "path",
          "status_code",
          "duration_ms",
          "timestamp"
        ],
        "type": "object"
      },
      "ResolvedContentPolicy": {
        "description": "The EFFECTIVE content policy for a screen + its provenance. Whole-layer\ncoalescing: the effective policy comes from exactly ONE layer.\n",
        "properties": {
          "effective": {
            "additionalProperties": true,
            "description": "All content_preferences keys + blocked_iab_categories.",
            "type": "object"
          },
          "object": {
            "description": "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 }`.",
            "example": "resolved_content_policy",
            "type": "string"
          },
          "provenance": {
            "description": "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`.",
            "properties": {
              "group_id": {
                "description": "Present only when source=group.",
                "type": "string"
              },
              "source": {
                "enum": [
                  "screen",
                  "group",
                  "network",
                  "default"
                ],
                "type": "string"
              }
            },
            "type": "object"
          },
          "screen_id": {
            "description": "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.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ResolvedSensingConfig": {
        "description": "The EFFECTIVE sensing view for a screen + its provenance (screen\ndeployment ?? group sensing config ?? default).\n",
        "properties": {
          "effective": {
            "description": "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.",
            "properties": {
              "capture_interval_ms": {
                "nullable": true,
                "type": "integer"
              },
              "estimated_fps_impact": {
                "nullable": true,
                "type": "number"
              },
              "min_confidence": {
                "nullable": true,
                "type": "number"
              },
              "profile_name": {
                "nullable": true,
                "type": "string"
              },
              "report_interval_ms": {
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "object": {
            "description": "Stripe-style type discriminator; always the literal `resolved_sensing_config`, so it is safe to switch on.",
            "example": "resolved_sensing_config",
            "type": "string"
          },
          "provenance": {
            "description": "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.",
            "properties": {
              "group_id": {
                "description": "Present only when source=group_default.",
                "type": "string"
              },
              "source": {
                "enum": [
                  "screen",
                  "group_default",
                  "default"
                ],
                "type": "string"
              }
            },
            "type": "object"
          },
          "screen_id": {
            "description": "The screen's Mongo id — the same identifier the route surfaces call `screenMongoId`. Echoes the path parameter.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "RolePermissions": {
        "description": "Permission flags for each role. Owners have all permissions,\nadmins have operational permissions, viewers have read-only access.\n",
        "properties": {
          "configure_webhooks": {
            "description": "Can configure webhooks",
            "type": "boolean"
          },
          "edit_screens": {
            "description": "Can edit screen settings",
            "type": "boolean"
          },
          "manage_team": {
            "description": "Can invite/remove team members",
            "type": "boolean"
          },
          "request_payouts": {
            "description": "Can request payouts",
            "type": "boolean"
          },
          "rotate_api_keys": {
            "description": "Can rotate API keys",
            "type": "boolean"
          },
          "view_analytics": {
            "description": "Can view impression analytics",
            "type": "boolean"
          },
          "view_earnings": {
            "description": "Can view earnings/revenue",
            "type": "boolean"
          },
          "view_impressions": {
            "description": "Can view impression data",
            "type": "boolean"
          },
          "view_payouts": {
            "description": "Can view payout history",
            "type": "boolean"
          },
          "view_screen_status": {
            "description": "Can view online/offline status",
            "type": "boolean"
          },
          "view_screens": {
            "description": "Can view screen list",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "SDKConfig": {
        "properties": {
          "ad_interval": {
            "description": "Seconds between ads",
            "type": "integer"
          },
          "allowed_ad_types": {
            "description": "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`.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "cache_size": {
            "description": "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`.",
            "type": "integer"
          },
          "image_duration": {
            "description": "Seconds to display image ads",
            "type": "integer"
          },
          "max_ads_per_hour": {
            "description": "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`.",
            "type": "integer"
          },
          "overlay_button_text": {
            "description": "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`.",
            "type": "string"
          },
          "sound_enabled": {
            "description": "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`.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "Screen": {
        "description": "A partner screen — the client-facing projection of a registered\ndevice+screen pair (`@trillboards/api-client` `Screen`). The screens\nresource reads the partner device endpoints.\n",
        "properties": {
          "city": {
            "description": "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`.",
            "type": "string"
          },
          "country": {
            "description": "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`.",
            "type": "string"
          },
          "created_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "device_id": {
            "description": "The paired device identifier.",
            "type": "string"
          },
          "embed_url": {
            "description": "Player embed URL for the screen.",
            "format": "uri",
            "type": "string"
          },
          "name": {
            "description": "Friendly screen/venue name.",
            "type": "string"
          },
          "resolution": {
            "description": "Display resolution label (e.g. \"1920x1080\") when known.",
            "type": "string"
          },
          "screen_id": {
            "description": "Stable screen identifier.",
            "type": "string"
          },
          "status": {
            "description": "Screen lifecycle status (e.g. pending, active, offline, suspended).",
            "type": "string"
          },
          "timezone": {
            "description": "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.",
            "type": "string"
          },
          "updated_at": {
            "description": "`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.",
            "format": "date-time",
            "type": "string"
          },
          "venue_type": {
            "description": "Granular OpenOOH venue type of the screen's location.",
            "type": "string"
          }
        },
        "required": [
          "screen_id",
          "name",
          "status",
          "created_at",
          "updated_at"
        ],
        "type": "object"
      },
      "ScreenAnalyticsResponse": {
        "properties": {
          "impressions": {
            "description": "`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.",
            "properties": {
              "daily_average": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              },
              "trend_percent": {
                "description": "Percentage change compared to previous period",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "period": {
            "description": "`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.",
            "properties": {
              "days": {
                "type": "integer"
              },
              "end": {
                "format": "date-time",
                "type": "string"
              },
              "start": {
                "format": "date-time",
                "type": "string"
              }
            },
            "type": "object"
          },
          "screen": {
            "description": "`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.",
            "properties": {
              "id": {
                "type": "string"
              },
              "location": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "online_status": {
                "type": "string"
              },
              "status": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "timeline": {
            "description": "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.",
            "items": {
              "properties": {
                "avg_duration_ms": {
                  "type": "integer"
                },
                "date": {
                  "format": "date",
                  "type": "string"
                },
                "impressions": {
                  "type": "integer"
                }
              },
              "type": "object"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "ScreenGroup": {
        "description": "A reusable, named, partner-scoped set of screens that analytics,\nmoderation policy, sensing/model deployment, fleet commands, and\nsensing routes can target. Members are always a subset of the\npartner's own screens.\n",
        "properties": {
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "description": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "description": "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.",
            "example": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
            "type": "string"
          },
          "name": {
            "description": "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.",
            "type": "string"
          },
          "object": {
            "description": "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.",
            "example": "screen_group",
            "type": "string"
          },
          "screen_count": {
            "description": "Distinct member screens (COUNT and member list share one junction — they cannot diverge).",
            "type": "integer"
          },
          "updated_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ScreenGroupContentPolicy": {
        "description": "The typed content/moderation policy a screen group carries (Phase 4 live\npolicy). `null` when the group has no policy of its own. Member screens\ninherit it at read/serve time when they have no explicit own policy\n(screen ?? group ?? partner network default ?? system default). Mirrors\nthe per-screen content_preferences keys 1:1; blocked_iab_categories is\nfirst-class here (the auction bcat).\n",
        "nullable": true,
        "properties": {
          "allow_adult_content": {
            "description": "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`.",
            "type": "boolean"
          },
          "allow_alcohol": {
            "description": "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).",
            "type": "boolean"
          },
          "allow_gambling": {
            "description": "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`.",
            "type": "boolean"
          },
          "allow_political": {
            "description": "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).",
            "type": "boolean"
          },
          "allow_profanity": {
            "description": "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`.",
            "type": "boolean"
          },
          "allow_religious": {
            "description": "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).",
            "type": "boolean"
          },
          "allow_tobacco": {
            "description": "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.",
            "type": "boolean"
          },
          "allow_violence": {
            "description": "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`.",
            "type": "boolean"
          },
          "allowed_categories": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "allowed_keywords": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "allowed_subjects": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "auto_approve_known_brands": {
            "description": "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.",
            "type": "boolean"
          },
          "blocked_categories": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "blocked_iab_categories": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "blocked_keywords": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "blocked_subjects": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "content_filtering_enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "group_id": {
            "description": "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.",
            "type": "string"
          },
          "max_age_rating": {
            "description": "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`.",
            "enum": [
              "G",
              "PG",
              "PG-13",
              "R",
              "NC-17"
            ],
            "type": "string"
          },
          "object": {
            "description": "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.",
            "example": "screen_group_content_policy",
            "type": "string"
          },
          "preference_level": {
            "description": "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.",
            "enum": [
              "strict",
              "moderate",
              "lenient",
              "permissive"
            ],
            "type": "string"
          },
          "updated_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ScreenGroupContentPolicyInput": {
        "description": "Partial, non-clobbering — only the fields present are written. Same keys\nthe per-screen content-preferences PUT accepts, plus blocked_iab_categories.\n",
        "properties": {
          "allow_adult_content": {
            "description": "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.",
            "type": "boolean"
          },
          "allow_alcohol": {
            "description": "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`.",
            "type": "boolean"
          },
          "allow_gambling": {
            "description": "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.",
            "type": "boolean"
          },
          "allow_political": {
            "description": "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.",
            "type": "boolean"
          },
          "allow_profanity": {
            "description": "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.",
            "type": "boolean"
          },
          "allow_religious": {
            "description": "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.",
            "type": "boolean"
          },
          "allow_tobacco": {
            "description": "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.",
            "type": "boolean"
          },
          "allow_violence": {
            "description": "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.",
            "type": "boolean"
          },
          "allowed_categories": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "type": "array"
          },
          "allowed_keywords": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "type": "array"
          },
          "allowed_subjects": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "type": "array"
          },
          "auto_approve_known_brands": {
            "description": "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.",
            "type": "boolean"
          },
          "blocked_categories": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "type": "array"
          },
          "blocked_iab_categories": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "type": "array"
          },
          "blocked_keywords": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "type": "array"
          },
          "blocked_subjects": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "type": "array"
          },
          "content_filtering_enabled": {
            "description": "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\".",
            "type": "boolean"
          },
          "max_age_rating": {
            "description": "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`.",
            "enum": [
              "G",
              "PG",
              "PG-13",
              "R",
              "NC-17"
            ],
            "type": "string"
          },
          "preference_level": {
            "description": "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.",
            "enum": [
              "strict",
              "moderate",
              "lenient",
              "permissive"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "ScreenGroupEvent": {
        "description": "Webhook body for a Screen Group lifecycle event (`screen_group.created` | `updated` |\n`deleted` | `member.added` | `member.removed` | `content_policy.updated` |\n`sensing_config.updated`). Standard `{ id, event, timestamp, data }` envelope: `id` (`evt_…`)\nis the idempotency key, and `data` is the Stripe-style `screen_group` /\n`screen_group_content_policy` / `screen_group_sensing_config` object the REST API returns.\nThe event type is also in the `X-Trillboards-Event` header — verify the compound\n`X-Trillboards-Signature` over the raw body first.\n",
        "example": {
          "data": {
            "added": 2,
            "added_screen_ids": [
              "6a246ab97096f4a811d39469",
              "6a246ab97096f4a811d3946a"
            ],
            "id": "sg_1a2b3c4d",
            "object": "screen_group",
            "rejected": []
          },
          "event": "screen_group.member.added",
          "id": "evt_lz9k2p_a1b2c3d4",
          "timestamp": "2026-07-01T12:34:56.789Z"
        },
        "properties": {
          "data": {
            "description": "The screen_group / content-policy / sensing-config object.",
            "type": "object"
          },
          "event": {
            "description": "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.",
            "enum": [
              "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"
            ],
            "type": "string"
          },
          "id": {
            "description": "`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.",
            "example": "evt_lz9k2p_a1b2c3d4",
            "type": "string"
          },
          "timestamp": {
            "description": "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`.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ScreenGroupSensingConfig": {
        "description": "A screen group's sensing profile binding (profile_name) + typed\nperformance overrides. `null` when the group has none. Member screens\ninherit via the sensing resolver's group step when they have no\nscreen-specific deployment.\n",
        "nullable": true,
        "properties": {
          "capture": {
            "description": "Edge frame-capture cadence overrides pushed to member screens\n(SensingConfig.capture keys, milliseconds). Group wins over the\nprofile default per-key when set.\n",
            "nullable": true,
            "properties": {
              "faceTriggeredIntervalMs": {
                "nullable": true,
                "type": "integer"
              },
              "minCaptureIntervalMs": {
                "nullable": true,
                "type": "integer"
              },
              "periodicIntervalMs": {
                "nullable": true,
                "type": "integer"
              },
              "vehicleTriggeredIntervalMs": {
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "capture_interval_ms": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "estimated_fps_impact": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "group_id": {
            "description": "The `sg_`-prefixed Screen Group this config belongs to; echoes the `groupId` path parameter.",
            "type": "string"
          },
          "min_confidence": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "object": {
            "description": "Stripe-style type discriminator; always the literal `screen_group_sensing_config`, so it is safe to switch on.",
            "example": "screen_group_sensing_config",
            "type": "string"
          },
          "profile_name": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "report_interval_ms": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "updated_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "vision_budget": {
            "description": "Per-screen cloud-vision budget for member screens. screen_hourly_pace\nwins when both are set; otherwise derived from screen_daily_calls.\nnull = no group budget (fleet keeps platform-default caps).\n",
            "nullable": true,
            "properties": {
              "screen_daily_calls": {
                "nullable": true,
                "type": "integer"
              },
              "screen_hourly_pace": {
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "ScreenGroupSensingConfigInput": {
        "description": "Partial, non-clobbering. A null override means \"inherit that field from\nthe base profile\". profile_name must resolve to a builtin or an existing\ncustom profile (else 400).\n",
        "properties": {
          "capture": {
            "description": "Edge frame-capture cadence block (milliseconds); null clears it.\nUnknown keys are rejected (400).\n",
            "nullable": true,
            "properties": {
              "faceTriggeredIntervalMs": {
                "maximum": 3600000,
                "minimum": 100,
                "nullable": true,
                "type": "integer"
              },
              "minCaptureIntervalMs": {
                "maximum": 3600000,
                "minimum": 100,
                "nullable": true,
                "type": "integer"
              },
              "periodicIntervalMs": {
                "maximum": 3600000,
                "minimum": 100,
                "nullable": true,
                "type": "integer"
              },
              "vehicleTriggeredIntervalMs": {
                "maximum": 3600000,
                "minimum": 100,
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "capture_interval_ms": {
            "description": "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.",
            "maximum": 3600000,
            "minimum": 100,
            "nullable": true,
            "type": "integer"
          },
          "enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "estimated_fps_impact": {
            "description": "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.",
            "minimum": 0,
            "nullable": true,
            "type": "number"
          },
          "min_confidence": {
            "description": "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).",
            "maximum": 1,
            "minimum": 0,
            "nullable": true,
            "type": "number"
          },
          "profile_name": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "report_interval_ms": {
            "description": "Observation-reporting cadence override in milliseconds (100..3600000); `null` means inherit from the base profile.",
            "maximum": 3600000,
            "minimum": 100,
            "nullable": true,
            "type": "integer"
          },
          "vision_budget": {
            "description": "Per-screen cloud-vision budget block; null clears it. Unknown keys\nare rejected (400).\n",
            "nullable": true,
            "properties": {
              "screen_daily_calls": {
                "maximum": 100000,
                "minimum": 1,
                "nullable": true,
                "type": "integer"
              },
              "screen_hourly_pace": {
                "maximum": 10000,
                "minimum": 1,
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "SdkApi.EnrichBody": {
        "additionalProperties": false,
        "properties": {
          "geohash6": {
            "description": "Geohash precision-6 cell (~1.2 km).",
            "example": "9q8yyz",
            "maxLength": 6,
            "minLength": 6,
            "pattern": "^[0-9a-z]+$",
            "type": "string"
          }
        },
        "type": "object"
      },
      "SdkApi.EnrichResponse": {
        "properties": {
          "connectionType": {
            "nullable": true,
            "type": "string"
          },
          "educationTier": {
            "nullable": true,
            "type": "string"
          },
          "geohash6": {
            "nullable": true,
            "type": "string"
          },
          "householdId": {
            "type": "string"
          },
          "iabAgeSegmentId": {
            "nullable": true,
            "type": "string"
          },
          "iabAgeSegmentName": {
            "nullable": true,
            "type": "string"
          },
          "iabIncomeSegmentId": {
            "nullable": true,
            "type": "string"
          },
          "iabIncomeSegmentName": {
            "nullable": true,
            "type": "string"
          },
          "source": {
            "description": "Geolocation database behind the record: `maxmind` (MaxMind GeoLite2), `dbip` (IP Geolocation by DB-IP, https://db-ip.com, licensed CC BY 4.0), or `ipinfo` when neither database is loaded.",
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "geohash6",
          "iabIncomeSegmentId",
          "iabIncomeSegmentName",
          "iabAgeSegmentId",
          "iabAgeSegmentName",
          "educationTier",
          "connectionType",
          "householdId",
          "source"
        ],
        "type": "object"
      },
      "SdkApi.ImpressionBody": {
        "additionalProperties": false,
        "properties": {
          "adId": {
            "minLength": 1,
            "type": "string"
          },
          "audioSampleRate": {
            "maximum": 384000,
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          },
          "bleDeviceCount": {
            "default": 0,
            "maximum": 10000,
            "minimum": 0,
            "type": "integer"
          },
          "bleProximityConfidence": {
            "default": 0,
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "bleTopDeviceRssi": {
            "maximum": 20,
            "minimum": -127,
            "nullable": true,
            "type": "integer"
          },
          "canvasHash": {
            "maxLength": 128,
            "nullable": true,
            "type": "string"
          },
          "channel": {
            "default": "ctv",
            "enum": [
              "ctv",
              "dooh",
              "web"
            ],
            "type": "string"
          },
          "completionRatio": {
            "default": 0,
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "connectionType": {
            "enum": [
              "residential",
              "mobile",
              "commercial",
              "vpn",
              "proxy",
              "unknown",
              null
            ],
            "nullable": true,
            "type": "string"
          },
          "csiMotionScore": {
            "maximum": 1,
            "minimum": 0,
            "nullable": true,
            "type": "number"
          },
          "csiOccupantCount": {
            "maximum": 1000,
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          },
          "csiSignalQuality": {
            "maximum": 1,
            "minimum": 0,
            "nullable": true,
            "type": "number"
          },
          "deviceFingerprint": {
            "maxLength": 128,
            "nullable": true,
            "type": "string"
          },
          "downlinkMbps": {
            "maximum": 10000,
            "minimum": 0,
            "nullable": true,
            "type": "number"
          },
          "durationMs": {
            "maximum": 3600000,
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          },
          "educationTier": {
            "nullable": true,
            "type": "string"
          },
          "effectiveType": {
            "nullable": true,
            "type": "string"
          },
          "householdId": {
            "maxLength": 128,
            "nullable": true,
            "type": "string"
          },
          "iabAgeSegmentId": {
            "nullable": true,
            "type": "string"
          },
          "iabAgeSegmentName": {
            "nullable": true,
            "type": "string"
          },
          "iabIncomeSegmentId": {
            "nullable": true,
            "type": "string"
          },
          "iabIncomeSegmentName": {
            "nullable": true,
            "type": "string"
          },
          "imaImpressionConfirmed": {
            "default": false,
            "type": "boolean"
          },
          "impressionId": {
            "format": "uuid",
            "type": "string"
          },
          "placementId": {
            "minLength": 1,
            "type": "string"
          },
          "qrBeaconSent": {
            "default": false,
            "type": "boolean"
          },
          "qrScanId": {
            "format": "uuid",
            "nullable": true,
            "type": "string"
          },
          "rttMs": {
            "maximum": 60000,
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          },
          "schemaVersion": {
            "default": "0.1",
            "type": "string"
          },
          "sdkVersion": {
            "type": "string"
          },
          "sessionId": {
            "format": "uuid",
            "type": "string"
          },
          "timestamp": {
            "format": "date-time",
            "type": "string"
          },
          "viewabilityReason": {
            "default": "ctv_incomplete",
            "type": "string"
          },
          "viewable": {
            "default": false,
            "type": "boolean"
          },
          "webglRenderer": {
            "maxLength": 512,
            "nullable": true,
            "type": "string"
          },
          "webglVendor": {
            "maxLength": 256,
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "impressionId",
          "adId",
          "placementId"
        ],
        "type": "object"
      },
      "SdkApi.ImpressionResponse": {
        "properties": {
          "frequency": {
            "minimum": 0,
            "type": "integer"
          },
          "givtPassed": {
            "type": "boolean"
          },
          "householdReach": {
            "minimum": 0,
            "type": "integer"
          },
          "impressionId": {
            "type": "string"
          },
          "publisherReach": {
            "minimum": 0,
            "type": "integer"
          },
          "received": {
            "type": "boolean"
          }
        },
        "required": [
          "received",
          "impressionId",
          "givtPassed",
          "frequency",
          "householdReach",
          "publisherReach"
        ],
        "type": "object"
      },
      "SdkApi.QrBeaconBody": {
        "additionalProperties": false,
        "properties": {
          "batteryCharging": {
            "type": "boolean"
          },
          "batteryLevel": {
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "canvasHash": {
            "type": "string"
          },
          "colorDepth": {
            "type": "integer"
          },
          "connectionDownlink": {
            "type": "number"
          },
          "connectionRtt": {
            "type": "number"
          },
          "connectionType": {
            "type": "string"
          },
          "deviceMemory": {
            "type": "number"
          },
          "fingerprintId": {
            "type": "string"
          },
          "hardwareConcurrency": {
            "type": "integer"
          },
          "languages": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "localIpRange": {
            "type": "string"
          },
          "maxTouchPoints": {
            "type": "integer"
          },
          "mediaDeviceCount": {
            "type": "integer"
          },
          "pixelRatio": {
            "type": "number"
          },
          "platform": {
            "type": "string"
          },
          "scanId": {
            "format": "uuid",
            "type": "string"
          },
          "screenHeight": {
            "type": "integer"
          },
          "screenWidth": {
            "type": "integer"
          },
          "timezone": {
            "type": "string"
          },
          "webglRenderer": {
            "type": "string"
          },
          "webglVendor": {
            "type": "string"
          }
        },
        "required": [
          "scanId"
        ],
        "type": "object"
      },
      "SdkApi.QrBeaconResponse": {
        "properties": {
          "edgeId": {
            "nullable": true,
            "type": "string"
          },
          "ipMatch": {
            "type": "boolean"
          },
          "updated": {
            "type": "boolean"
          }
        },
        "required": [
          "updated"
        ],
        "type": "object"
      },
      "SdkApi.QrScanBody": {
        "additionalProperties": false,
        "properties": {
          "adId": {
            "minLength": 1,
            "type": "string"
          },
          "destinationUrl": {
            "format": "uri",
            "type": "string"
          },
          "placementId": {
            "minLength": 1,
            "type": "string"
          },
          "scanId": {
            "format": "uuid",
            "type": "string"
          },
          "screenId": {
            "description": "Publisher screen identifier (≤24 chars) — ties the scan to the rendering screen.",
            "maxLength": 24,
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "scanId",
          "screenId",
          "adId",
          "placementId",
          "destinationUrl"
        ],
        "type": "object"
      },
      "SdkApi.QrScanResponse": {
        "properties": {
          "id": {
            "type": "string"
          },
          "scanId": {
            "type": "string"
          }
        },
        "required": [
          "scanId",
          "id"
        ],
        "type": "object"
      },
      "SensingApi.PairedTrainingBody": {
        "additionalProperties": false,
        "properties": {
          "samples": {
            "description": "Batch of paired samples. At most 1000 samples per request.",
            "items": {
              "$ref": "#/components/schemas/SensingApi.PairedTrainingSample"
            },
            "maxItems": 1000,
            "minItems": 1,
            "type": "array"
          },
          "screenId": {
            "description": "Optional screen binding hint. The collector may upload samples before the device is bound to a screen, in which case this field is omitted.",
            "maxLength": 48,
            "type": "string"
          }
        },
        "required": [
          "samples"
        ],
        "type": "object"
      },
      "SensingApi.PairedTrainingResponse": {
        "properties": {
          "acceptedCount": {
            "minimum": 0,
            "type": "integer"
          },
          "deduped": {
            "minimum": 0,
            "type": "integer"
          },
          "ok": {
            "type": "boolean"
          },
          "received": {
            "type": "boolean"
          },
          "rejectedCount": {
            "minimum": 0,
            "type": "integer"
          },
          "sampleIds": {
            "items": {
              "format": "uuid",
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "received",
          "ok",
          "acceptedCount",
          "deduped",
          "rejectedCount",
          "sampleIds"
        ],
        "type": "object"
      },
      "SensingApi.PairedTrainingSample": {
        "additionalProperties": false,
        "properties": {
          "cameraModelVersion": {
            "maxLength": 128,
            "type": "string"
          },
          "csiWindowBase64": {
            "description": "Base64-encoded CSI window bytes. Length must equal `subcarrierCount * 20 frames * 4 bytes`.",
            "maxLength": 200000,
            "minLength": 1,
            "type": "string"
          },
          "keypointConfidences": {
            "description": "Confidence per keypoint, length 17.",
            "items": {
              "type": "number"
            },
            "type": "array"
          },
          "keypoints": {
            "description": "Flattened 17×(x,y) body keypoints. Float array length must be 34.",
            "items": {
              "type": "number"
            },
            "type": "array"
          },
          "numCameraFrames": {
            "maximum": 1000,
            "minimum": 1,
            "type": "integer"
          },
          "overallConfidence": {
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "sampleId": {
            "description": "Client-generated UUID. Idempotency key.",
            "format": "uuid",
            "type": "string"
          },
          "subcarrierCount": {
            "description": "Number of OFDM subcarriers the device sampled.",
            "type": "integer"
          },
          "venueType": {
            "maxLength": 64,
            "type": "string"
          },
          "windowEndMs": {
            "minimum": 0,
            "type": "integer"
          },
          "windowStartMs": {
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "sampleId",
          "csiWindowBase64",
          "subcarrierCount",
          "keypoints",
          "keypointConfidences",
          "overallConfidence",
          "numCameraFrames",
          "windowStartMs",
          "windowEndMs"
        ],
        "type": "object"
      },
      "SensingPerformanceMode": {
        "description": "High-level sensing workload intent. Hardware tier and device telemetry are read-only server facts.",
        "enum": [
          "auto",
          "presence_only",
          "demographics",
          "advanced"
        ],
        "type": "string"
      },
      "SensingRoute": {
        "description": "A Sense Anything Route — a (condition → channel actions) alert/automation rule.",
        "properties": {
          "actions": {
            "description": "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`.",
            "items": {
              "$ref": "#/components/schemas/SensingRouteAction"
            },
            "type": "array"
          },
          "activeDaysMask": {
            "default": 127,
            "description": "One bit per weekday (127 = always).",
            "type": "integer"
          },
          "activeEndMinute": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "activeStartMinute": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "activeTz": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "condition": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SensingRouteCondition"
              }
            ],
            "description": "`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.",
            "nullable": true,
            "type": "object"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "debounceSeconds": {
            "default": 300,
            "description": "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.",
            "type": "integer"
          },
          "digest": {
            "description": "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.",
            "nullable": true,
            "properties": {
              "intervalMinutes": {
                "type": "integer"
              },
              "metric": {
                "enum": [
                  "count",
                  "avg",
                  "max"
                ],
                "type": "string"
              },
              "nextRunAt": {
                "format": "date-time",
                "type": "string"
              },
              "windowMinutes": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "enabled": {
            "description": "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).",
            "type": "boolean"
          },
          "mode": {
            "description": "`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 and explicit review timing are rejected on digest routes.",
            "enum": [
              "threshold",
              "digest"
            ],
            "type": "string"
          },
          "name": {
            "description": "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.",
            "type": "string"
          },
          "ownerId": {
            "description": "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.",
            "type": "string"
          },
          "ownerKind": {
            "description": "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.",
            "example": "partner",
            "type": "string"
          },
          "partnerId": {
            "description": "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.",
            "type": "string"
          },
          "profileName": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "reviewTimingMode": {
            "$ref": "#/components/schemas/SensingRouteReviewTimingMode"
          },
          "reviewWaitSeconds": {
            "$ref": "#/components/schemas/SensingRouteReviewWaitSeconds"
          },
          "routeId": {
            "description": "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.",
            "type": "string"
          },
          "screenFocusRegions": {
            "description": "Per-camera areas, at most one entry per screen. The legacy cloud path converts geometry into prompt text and is not geometrically enforced (measured 2026-08-25 on gemini-3.1-flash-lite). The retained local connector masks actual pixels separately for each declared scope; original unmasked JPEGs remain alert evidence. Retained installation and model accuracy require separate qualification.",
            "items": {
              "$ref": "#/components/schemas/SensingRouteScreenFocusRegion"
            },
            "type": "array"
          },
          "screenMongoIds": {
            "description": "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.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "sustainMinSamples": {
            "description": "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.",
            "type": "integer"
          },
          "sustainSeconds": {
            "description": "How long the condition must hold before the FIRST alert, in seconds (600 = ten minutes). Omit to alert on the first true reading. Cadence-independent, because the evaluator compares timestamps - correct however often the screen reports, including when that rate changes while the condition holds.",
            "maximum": 86400,
            "minimum": 1,
            "nullable": true,
            "type": "integer"
          },
          "sustainWindowSeconds": {
            "description": "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.",
            "type": "integer"
          },
          "targetGroupId": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "updatedAt": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "SensingRouteAction": {
        "description": "One channel action fired when the route matches.",
        "properties": {
          "attachClip": {
            "description": "Attach the evidence frame/clip (people-facing channels).",
            "type": "boolean"
          },
          "channel": {
            "description": "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.",
            "enum": [
              "webhook",
              "whatsapp",
              "sms",
              "slack",
              "email",
              "clip"
            ],
            "type": "string"
          },
          "recipientE164": {
            "description": "E.164 phone number (channel=whatsapp|sms).",
            "example": "+12025550123",
            "type": "string"
          },
          "recipientEmail": {
            "description": "Recipient email (channel=email).",
            "nullable": true,
            "type": "string"
          },
          "slackChannelId": {
            "description": "Slack channel id (channel=slack).",
            "nullable": true,
            "type": "string"
          },
          "templateName": {
            "description": "Message template name (managed WhatsApp/SMS).",
            "type": "string"
          },
          "webhookId": {
            "description": "Target webhook endpoint id (channel=webhook).",
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "channel"
        ],
        "type": "object"
      },
      "SensingRouteCondition": {
        "description": "A threshold condition — the route fires when the sensed field crosses the value.",
        "properties": {
          "field": {
            "description": "The sensed field key (snake_case) to evaluate.",
            "example": "loiter_count",
            "type": "string"
          },
          "op": {
            "description": "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.",
            "enum": [
              "gt",
              "gte",
              "lt",
              "lte",
              "eq",
              "neq"
            ],
            "type": "string"
          },
          "value": {
            "description": "The threshold value (numeric, string, or boolean depending on the field).",
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              },
              {
                "type": "boolean"
              }
            ]
          }
        },
        "required": [
          "field",
          "op"
        ],
        "type": "object"
      },
      "SensingRouteReviewTimingMode": {
        "description": "Threshold-alert timing. NULL preserves legacy behavior; immediate adds no check delay; wait_recheck requires distinct first/follow-up positive images at least reviewWaitSeconds apart on capture time, not uninterrupted activity or message grouping.",
        "enum": [
          "immediate",
          "wait_recheck",
          null
        ],
        "nullable": true,
        "type": "string"
      },
      "SensingRouteReviewWaitSeconds": {
        "description": "Whole seconds before a fresh follow-up check. Required only for wait_recheck; must be NULL for immediate or unspecified timing. These two settings must be updated together.",
        "maximum": 600,
        "minimum": 1,
        "nullable": true,
        "type": "integer"
      },
      "SensingRouteScreenFocusRegion": {
        "description": "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.",
        "properties": {
          "height": {
            "description": "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.",
            "format": "double",
            "maximum": 1,
            "minimum": 0.05,
            "type": "number"
          },
          "points": {
            "description": "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.",
            "items": {
              "properties": {
                "x": {
                  "format": "double",
                  "maximum": 1,
                  "minimum": 0,
                  "type": "number"
                },
                "y": {
                  "format": "double",
                  "maximum": 1,
                  "minimum": 0,
                  "type": "number"
                }
              },
              "required": [
                "x",
                "y"
              ],
              "type": "object"
            },
            "maxItems": 8,
            "minItems": 3,
            "type": "array"
          },
          "screenMongoId": {
            "description": "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.",
            "type": "string"
          },
          "width": {
            "description": "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.",
            "format": "double",
            "maximum": 1,
            "minimum": 0.05,
            "type": "number"
          },
          "x": {
            "description": "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.",
            "format": "double",
            "maximum": 0.95,
            "minimum": 0,
            "type": "number"
          },
          "y": {
            "description": "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.",
            "format": "double",
            "maximum": 0.95,
            "minimum": 0,
            "type": "number"
          }
        },
        "required": [
          "screenMongoId",
          "x",
          "y",
          "width",
          "height"
        ],
        "type": "object"
      },
      "SensingSettingsResponse": {
        "properties": {
          "active_profile": {
            "description": "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.",
            "nullable": true,
            "properties": {
              "capabilities": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "capture_interval_ms": {
                "nullable": true,
                "type": "integer"
              },
              "profile_id": {
                "nullable": true,
                "type": "string"
              },
              "profile_name": {
                "nullable": true,
                "type": "string"
              },
              "report_interval_ms": {
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "desired_mode": {
            "$ref": "#/components/schemas/SensingPerformanceMode"
          },
          "device_capability": {
            "description": "Read-only latest device telemetry used for safety checks.",
            "properties": {
              "agent_version": {
                "nullable": true,
                "type": "string"
              },
              "gpu_delegate_supported": {
                "nullable": true,
                "type": "boolean"
              },
              "gpu_name": {
                "nullable": true,
                "type": "string"
              },
              "model_tier": {
                "nullable": true,
                "type": "string"
              },
              "recent_failure_count": {
                "type": "integer"
              },
              "soc_name": {
                "nullable": true,
                "type": "string"
              },
              "soc_vendor": {
                "nullable": true,
                "type": "string"
              },
              "telemetry_fresh": {
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "effective_mode": {
            "$ref": "#/components/schemas/SensingPerformanceMode"
          },
          "last_apply_status": {
            "description": "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.",
            "nullable": true,
            "type": "object"
          },
          "modes": {
            "description": "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.",
            "items": {
              "properties": {
                "label": {
                  "type": "string"
                },
                "value": {
                  "$ref": "#/components/schemas/SensingPerformanceMode"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "safety": {
            "description": "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`.",
            "properties": {
              "allowed": {
                "type": "boolean"
              },
              "blocks": {
                "items": {
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                },
                "type": "array"
              },
              "warnings": {
                "items": {
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                },
                "type": "array"
              }
            },
            "type": "object"
          },
          "screen_id": {
            "description": "The screen's Mongo id — the same identifier the route surfaces call `screenMongoId`. Echoes the path parameter.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "SensingThresholdCrossedEvent": {
        "description": "Webhook request BODY delivered when a Sense Anything Route fires. Unlike the\ndevice/programmatic events, a Route fire delivers a FLAT, self-describing\npayload (no `{event,timestamp,data}` wrapper) — the event type is in the\n`X-Trillboards-Event` request header (`sensing.threshold_crossed` for a custom\nsensed field, or the matching `audience.*` / `venue.*` event for a known\nsemantic; a scheduled-digest Route delivers `venue.traffic_summary`). All Route\nevents share this same body shape. Verify the `X-Trillboards-Signature` header\nover the raw body before trusting it.\n",
        "example": {
          "field": "loiter_count",
          "fired_at": "2026-06-08T20:15:00.000Z",
          "observed": 7,
          "op": "gt",
          "owner_kind": "partner",
          "route_id": "rt_8f2a1c",
          "route_name": "Loiter after 8pm",
          "screen_id": "6a246ab97096f4a811d39469",
          "threshold": 5
        },
        "properties": {
          "field": {
            "description": "The sensed field key evaluated (null for count-only digests).",
            "nullable": true,
            "type": "string"
          },
          "fired_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "observed": {
            "description": "The observed value that crossed the threshold."
          },
          "op": {
            "description": "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.",
            "enum": [
              "gt",
              "gte",
              "lt",
              "lte",
              "eq",
              "neq"
            ],
            "nullable": true,
            "type": "string"
          },
          "owner_kind": {
            "description": "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.",
            "enum": [
              "earner_user",
              "partner"
            ],
            "type": "string"
          },
          "route_id": {
            "description": "The Route that fired.",
            "type": "string"
          },
          "route_name": {
            "description": "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.",
            "type": "string"
          },
          "screen_id": {
            "description": "The screen (mongo id) whose observation crossed the threshold.",
            "type": "string"
          },
          "threshold": {
            "description": "The configured threshold value (number, string, or boolean). May be null."
          }
        },
        "type": "object"
      },
      "SsdpDeviceInfo": {
        "additionalProperties": true,
        "properties": {
          "friendlyName": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "location": {
            "description": "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.",
            "type": "string"
          },
          "manufacturer": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "modelName": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "server": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "st": {
            "description": "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.",
            "type": "string"
          },
          "udn": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "location",
          "st"
        ],
        "type": "object"
      },
      "TeamInviteResponse": {
        "properties": {
          "email": {
            "description": "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.",
            "format": "email",
            "type": "string"
          },
          "expires_at": {
            "description": "When the invitation expires (7 days from creation)",
            "format": "date-time",
            "type": "string"
          },
          "membership_id": {
            "description": "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.",
            "type": "string"
          },
          "role": {
            "description": "Only `admin` and `viewer` can be invited — `owner` is the membership created with the partner account and cannot be granted through this endpoint.",
            "enum": [
              "admin",
              "viewer"
            ],
            "type": "string"
          },
          "status": {
            "description": "`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.",
            "enum": [
              "pending",
              "active"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "TeamMember": {
        "properties": {
          "accepted_at": {
            "description": "`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.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "email": {
            "description": "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.",
            "format": "email",
            "type": "string"
          },
          "invited_at": {
            "description": "When the invitation row was created. Re-inviting the same address rotates the token and moves this timestamp forward.",
            "format": "date-time",
            "type": "string"
          },
          "invited_by": {
            "description": "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.",
            "nullable": true,
            "properties": {
              "email": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "last_portal_access": {
            "description": "**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.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "membership_id": {
            "description": "Unique membership record ID",
            "type": "string"
          },
          "name": {
            "description": "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.",
            "type": "string"
          },
          "permissions": {
            "$ref": "#/components/schemas/RolePermissions"
          },
          "photo": {
            "description": "Profile photo URL from the linked user account, and null/absent while the invitation is pending for the same reason as `name`.",
            "nullable": true,
            "type": "string"
          },
          "role": {
            "description": "`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`.",
            "enum": [
              "owner",
              "admin",
              "viewer"
            ],
            "type": "string"
          },
          "status": {
            "description": "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.",
            "enum": [
              "active",
              "pending",
              "revoked"
            ],
            "type": "string"
          },
          "user_id": {
            "description": "User ID (null if pending invitation)",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Transaction": {
        "properties": {
          "amount_cents": {
            "description": "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.",
            "type": "integer"
          },
          "created_at": {
            "description": "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.",
            "format": "date-time",
            "type": "string"
          },
          "date": {
            "description": "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.",
            "format": "date",
            "type": "string"
          },
          "earning_source_version": {
            "enum": [
              1
            ],
            "type": "integer"
          },
          "earning_sources": {
            "$ref": "#/components/schemas/EarningsSources"
          },
          "id": {
            "description": "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>`.",
            "type": "string"
          },
          "impressions": {
            "description": "Number of impressions (for earning transactions)",
            "type": "integer"
          },
          "source": {
            "description": "Source of the transaction (e.g., impressions, payout)",
            "type": "string"
          },
          "stripe_transfer_id": {
            "description": "Stripe transfer ID (for payout transactions)",
            "type": "string"
          },
          "type": {
            "description": "`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.",
            "enum": [
              "earning",
              "payout"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "TrillboardsError": {
        "description": "The canonical Trillboards error envelope. Emitted by `middleware/globalErrorHandler.js` for every error that reaches it: `utils/apiErrors.ts` `toJSON()` builds the inner object and the handler wraps it in `{ error }` and stamps `request_id`.",
        "properties": {
          "error": {
            "properties": {
              "code": {
                "description": "Stable machine-readable error code. Switch on this, never on `message`. The 143 CATALOGUED codes are published as the `ErrorCode` schema and served live by `GET /v1/errors`, which is the authority. This field is NOT limited to them: services emit further codes directly, and `ApiError.notFound(resource)` mints `<resource>_not_found` from an arbitrary resource name. Always handle an unrecognised code by falling back to `type`.",
                "example": "not_found",
                "type": "string",
                "x-error-code-catalog": "#/components/schemas/ErrorCode"
              },
              "details": {
                "description": "Per-field violations (populated from Zod errors).",
                "items": {
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "param": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                },
                "type": "array"
              },
              "doc_url": {
                "description": "Documentation for this code — https://api.trillboards.com/docs/errors#<code>.",
                "example": "https://api.trillboards.com/docs/errors#AUTH_REQUIRED",
                "format": "uri",
                "type": "string"
              },
              "help": {
                "description": "Agent-facing hints and suggestions.",
                "properties": {
                  "portal_url": {
                    "format": "uri",
                    "type": "string"
                  },
                  "suggestions": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "summary": {
                    "type": "string"
                  },
                  "try_next": {
                    "type": "string"
                  }
                },
                "type": "object"
              },
              "message": {
                "description": "Human-readable error message. Safe to surface to end-users.",
                "type": "string"
              },
              "param": {
                "description": "Offending parameter name, or null when the error is not field-scoped.",
                "nullable": true,
                "type": "string"
              },
              "request_id": {
                "description": "Echoed X-Request-Id. Quote this to support for log correlation.",
                "pattern": "^req_[a-f0-9]{32}$",
                "type": "string"
              },
              "type": {
                "$ref": "#/components/schemas/ErrorType"
              }
            },
            "required": [
              "type",
              "code",
              "message",
              "request_id"
            ],
            "type": "object"
          }
        },
        "required": [
          "error"
        ],
        "type": "object",
        "x-canonical-envelope": "error"
      },
      "UpdatePartnerSensingRouteRequest": {
        "description": "Partial, non-clobbering update — include only the fields to change. Update reviewTimingMode and reviewWaitSeconds together; omit both to preserve existing timing. Explicit timing cannot be added to a digest, and changing to a digest clears the prior event check timing.",
        "properties": {
          "actions": {
            "description": "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.",
            "items": {
              "$ref": "#/components/schemas/SensingRouteAction"
            },
            "type": "array"
          },
          "activeDaysMask": {
            "description": "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.",
            "maximum": 127,
            "minimum": 0,
            "type": "integer"
          },
          "activeEndMinute": {
            "description": "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.",
            "maximum": 1439,
            "minimum": 0,
            "type": "integer"
          },
          "activeStartMinute": {
            "description": "Inclusive start of the daily active window, minutes past local midnight in `activeTz` (0..1439). Requires `activeTz` to have any effect.",
            "maximum": 1439,
            "minimum": 0,
            "type": "integer"
          },
          "activeTz": {
            "description": "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.",
            "type": "string"
          },
          "condition": {
            "$ref": "#/components/schemas/SensingRouteCondition"
          },
          "debounceSeconds": {
            "description": "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.",
            "minimum": 0,
            "type": "integer"
          },
          "digest": {
            "description": "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.",
            "properties": {
              "field": {
                "type": "string"
              },
              "intervalMinutes": {
                "minimum": 5,
                "type": "integer"
              },
              "metric": {
                "enum": [
                  "count",
                  "avg",
                  "max"
                ],
                "type": "string"
              },
              "windowMinutes": {
                "minimum": 1,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "name": {
            "description": "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.",
            "maxLength": 200,
            "type": "string"
          },
          "profileName": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "reviewTimingMode": {
            "$ref": "#/components/schemas/SensingRouteReviewTimingMode"
          },
          "reviewWaitSeconds": {
            "$ref": "#/components/schemas/SensingRouteReviewWaitSeconds"
          },
          "screenFocusRegions": {
            "description": "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.",
            "items": {
              "$ref": "#/components/schemas/SensingRouteScreenFocusRegion"
            },
            "maxItems": 250,
            "type": "array"
          },
          "screenMongoIds": {
            "description": "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).",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "type": "array"
          },
          "sustainMinSamples": {
            "description": "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.",
            "minimum": 1,
            "type": "integer"
          },
          "sustainSeconds": {
            "description": "How long the condition must hold before the FIRST alert, in seconds (600 = ten minutes). Omit to alert on the first true reading. Cadence-independent, because the evaluator compares timestamps - correct however often the screen reports, including when that rate changes while the condition holds.",
            "maximum": 86400,
            "minimum": 1,
            "type": "integer"
          },
          "sustainWindowSeconds": {
            "description": "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.",
            "minimum": 0,
            "type": "integer"
          },
          "targetGroupId": {
            "description": "Set to a sg_ group id to (re)target the group's live members, or null to switch back to an explicit screenMongoIds set. Mutually exclusive with screenMongoIds in one request.",
            "nullable": true,
            "type": "string"
          }
        },
        "type": "object"
      },
      "VastWaterfall": {
        "description": "The resolved programmatic VAST waterfall. `sources` is an ORDERED ARRAY and clients MUST iterate ALL of it — by `priority` ascending, skipping `enabled: false` — advancing to the next source on no-fill, empty VAST, timeout, IMA load error AND playback error (VAST 303 / 300 / 301 / 1009 all mean \"try the next source\", never \"stop\"). Reading `sources[0]` and stopping is the most common integration defect and reads as \"no demand\" when demand exists further down. Apply each source's own `timeout_ms`, and cap the whole loop at `total_timeout_ms`. Always handle player error and never leave the screen black; always fire the VAST `<Error>` beacon with `[ERRORCODE]` substituted. Full contract: docs/integrations/partner-native-ima-sdk.md §0 and §4.1. SQL/Admin-enabled sources remain visible; request-pressure decisions are enforced, when enabled, at first-party upstream fetch seams rather than by removing sources from this catalog.\n",
        "nullable": true,
        "properties": {
          "enabled": {
            "description": "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.",
            "type": "boolean"
          },
          "sources": {
            "description": "Every source must be attempted, by `priority` ascending, until one actually plays. Not a preference list.",
            "items": {
              "$ref": "#/components/schemas/VastWaterfallSource"
            },
            "type": "array"
          },
          "total_timeout_ms": {
            "description": "Budget in ms for the ENTIRE waterfall loop, across all sources. When it is spent, stop trying sources and return to your own content.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "VastWaterfallSource": {
        "description": "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\".",
        "properties": {
          "auction_request_id": {
            "description": "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.",
            "type": "string"
          },
          "cpm": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "degraded": {
            "description": "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.",
            "type": "boolean"
          },
          "duration_seconds": {
            "description": "Whole seconds of the creative THIS source will serve, so you can schedule continuous loop playback without first fetching the VAST and probing the MP4. Present only on the first-party `trillboards_direct` source (a programmatic source's length is not known until its own VAST is fetched). It is the same number as the `<Duration>` in the VAST that `vast_url` returns, so use either — they cannot disagree. NOT a playback control: IMA still takes its real timing from the media, so treat this as scheduling metadata and keep honouring the actual quartile/complete events. A creative longer than your ad slot will still be cut off by the next poll; we warn the uploader at upload time when that is the case.\n",
            "type": "integer"
          },
          "enabled": {
            "description": "When false, skip this source entirely. Present-but-disabled sources may appear in the array.",
            "type": "boolean"
          },
          "error_rate_pct": {
            "description": "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.",
            "type": "number"
          },
          "fetch_mode": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "name": {
            "description": "Opaque public alias for the demand source. Echo it back as `variant_name` on `programmatic-event` reports.",
            "type": "string"
          },
          "priority": {
            "description": "Opaque ascending sort key — lowest is tried first. Do NOT treat it as an index: it may start at 0 (a source we want to win outright — the `?test=1` source, an injected first-party bid, an auction winner — is stamped `priority: 0`), and values may be non-contiguous (some paths renumber survivors 1..N, others keep catalog values). Only the relative order is meaningful; sort ascending and do no arithmetic on it.\n",
            "type": "integer"
          },
          "request_id": {
            "description": "Per-source request identifier. Report against the source you actually used, not `sources[0]`.",
            "type": "string"
          },
          "timeout_ms": {
            "description": "Per-source budget in ms for fetching and loading THIS source. Distinct from the waterfall-wide `total_timeout_ms`.",
            "type": "integer"
          },
          "vast_url": {
            "description": "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.",
            "format": "uri",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Webhook": {
        "properties": {
          "created_at": {
            "description": "When webhook was created",
            "format": "date-time",
            "type": "string"
          },
          "events": {
            "description": "Subscribed event types",
            "items": {
              "enum": [
                "device.online",
                "device.offline",
                "impression.recorded",
                "creative.rewards_ready",
                "campaign.allocated",
                "payout.processed",
                "sensing.threshold_crossed",
                "audience.spike",
                "audience.venue_busy",
                "audience.purchase_intent",
                "audience.demographics_update",
                "venue.traffic_summary",
                "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",
                "fleet_command.completed",
                "fleet_command.failed"
              ],
              "type": "string"
            },
            "type": "array"
          },
          "failure_count": {
            "description": "Total failed deliveries",
            "type": "integer"
          },
          "last_triggered_at": {
            "description": "Last time webhook was triggered",
            "format": "date-time",
            "type": "string"
          },
          "secret": {
            "description": "Webhook signing secret (whsec_…). Returned when the webhook is\ncreated so partners can verify the compound `X-Trillboards-Signature`\nheader over the raw payload (see the SDK's Webhooks.constructEvent).\nTreat as a credential — store it securely and never expose it\nclient-side.\n",
            "example": "whsec_m4k7x9a1b2c3d4e5f6",
            "type": "string"
          },
          "status": {
            "description": "Webhook status",
            "enum": [
              "active",
              "inactive"
            ],
            "type": "string"
          },
          "success_count": {
            "description": "Total successful deliveries",
            "type": "integer"
          },
          "url": {
            "description": "Webhook endpoint URL",
            "example": "https://example.com/webhooks/trillboards",
            "format": "uri",
            "type": "string"
          },
          "webhook_id": {
            "description": "Unique webhook identifier",
            "example": "wh_m4k7x9_a1b2c3d4e5f6",
            "type": "string"
          }
        },
        "type": "object"
      },
      "WebhookDelivery": {
        "properties": {
          "attempted_at": {
            "description": "When delivery was first attempted",
            "format": "date-time",
            "type": "string"
          },
          "delivered_at": {
            "description": "When delivery succeeded",
            "format": "date-time",
            "type": "string"
          },
          "error_message": {
            "description": "Error message if failed",
            "type": "string"
          },
          "event_type": {
            "description": "Event that triggered this delivery",
            "type": "string"
          },
          "http_status": {
            "description": "HTTP status code from endpoint",
            "type": "integer"
          },
          "id": {
            "description": "Delivery record ID",
            "type": "string"
          },
          "response_time_ms": {
            "description": "Response time in milliseconds",
            "type": "integer"
          },
          "retry_count": {
            "description": "Number of retry attempts",
            "type": "integer"
          },
          "status": {
            "description": "Delivery status",
            "enum": [
              "pending",
              "success",
              "failed",
              "retrying"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "WebhookEvent": {
        "description": "Standard webhook event payload sent to your endpoint.\nAll events follow this structure.\n",
        "example": {
          "data": {
            "device_id": "507f1f77bcf86cd799439011",
            "external_device_id": "machine-001",
            "fingerprint": "P_a1b2c3d4e5f6",
            "name": "Mall Kiosk #42"
          },
          "event": "device.online",
          "timestamp": "2026-01-01T02:30:00.000Z"
        },
        "properties": {
          "data": {
            "description": "Event-specific data",
            "type": "object"
          },
          "event": {
            "description": "Event type",
            "example": "device.online",
            "type": "string"
          },
          "timestamp": {
            "description": "When event occurred",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "WifiEnvironmentSnapshot": {
        "additionalProperties": true,
        "nullable": true,
        "properties": {
          "channelCongestionRatio": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "connectedChannelWidthMhz": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "connectedFrequencyMhz": {
            "description": "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`.",
            "nullable": true,
            "type": "integer"
          },
          "connectedLinkSpeedMbps": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "connectedSignalDbm": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "frequencyBand": {
            "description": "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.",
            "nullable": true,
            "type": "string"
          },
          "medianSignalDbm": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "networkCount": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "rssiVariance": {
            "description": "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.",
            "nullable": true,
            "type": "number"
          },
          "scanTimestampMs": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "signalSpreadDbm": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "uniqueBssidCount": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          }
        },
        "type": "object"
      },
      "WifiScanResult": {
        "additionalProperties": true,
        "properties": {
          "channelWidthMhz": {
            "description": "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.",
            "nullable": true,
            "type": "integer"
          },
          "frequencyMhz": {
            "description": "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.",
            "maximum": 7200,
            "minimum": 2400,
            "type": "integer"
          },
          "rawBssid": {
            "description": "The AP's BSSID exactly as Android reports it, unhashed on the device; Trillboards stores it as received and as a keyed HMAC-SHA256 hash. 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.",
            "type": "string"
          },
          "signalStrengthDbm": {
            "description": "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.",
            "maximum": 0,
            "minimum": -120,
            "type": "integer"
          }
        },
        "required": [
          "rawBssid",
          "signalStrengthDbm",
          "frequencyMhz"
        ],
        "type": "object"
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "description": "The same platform API key as `BearerAuth`, sent as a raw header value instead of a Bearer credential. `X-Trillboards-API-Key` is accepted as a legacy alias for this header. Scope semantics are identical.",
        "in": "header",
        "name": "X-API-Key",
        "type": "apiKey"
      },
      "BearerAuth": {
        "description": "API key authentication. Include your API key as a Bearer token:\n```\nAuthorization: Bearer trb_partner_xxxxx\n```\n",
        "scheme": "bearer",
        "type": "http"
      },
      "BearerAuth__sdk": {
        "description": "Platform API key sent as `Authorization: Bearer <key>`. This is the canonical carrier. Scopes listed on an operation are enforced by `middleware/apiKeyAuth.js`; ALL listed scopes must be held. A key holding a namespace wildcard (e.g. `sdk:*`) satisfies any scope in that namespace. An operation listing no scope accepts any valid, unexpired key.",
        "scheme": "bearer",
        "type": "http"
      },
      "DeviceFingerprint": {
        "description": "Device fingerprint for the federated-learning write path, resolved against a known, sensor-bound device. `screenId` in the request body and `screen_id` in the query string are accepted as equivalent carriers for fielded agents that cannot set the header.",
        "in": "header",
        "name": "X-Device-Fingerprint",
        "type": "apiKey"
      },
      "DeviceToken": {
        "description": "The registered device's credential: the device token `check-screen` issues\nto the partner SDK for a registered device (never a partner API key). Every\nrequest also carries `X-Device-Timestamp` (epoch ms, within the replay\nwindow) and a single-use `X-Device-Nonce`; a reused nonce is refused 409.\n",
        "in": "header",
        "name": "X-Device-Token",
        "type": "apiKey"
      },
      "DeviceToken__sensing": {
        "description": "Per-device token issued at pairing, sent by a fielded device. `X-Trillboard-Device-Token` is accepted as a legacy alias. In production these requests must also carry a replay guard — a timestamp (`X-Device-Timestamp`) and a nonce (`X-Device-Nonce`) — or the request is rejected 400 before authentication completes.",
        "in": "header",
        "name": "X-Device-Token",
        "type": "apiKey"
      },
      "UserJWT": {
        "description": "User JWT authentication for earner-to-partner upgrade.\nUsed only for the `/v2/earner/upgrade-to-partner` endpoint.\nThis is the same JWT token used for the Earner Portal.\n",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "contact": {
      "email": "api@trillboards.com",
      "name": "Trillboards API Support",
      "url": "https://trillboards.com/support"
    },
    "description": "Supply, on-glass SDK, sensing, device provisioning and federated learning.\n\nEverything you can call with a Trillboards **partner credential** — one\n`platform_api_key` (plus `sdk:*` scopes where noted) for the server-side surface,\nand a per-device token for the on-glass surfaces.\n\nThis is one document covering five previously separate references:\n\n| Section | Credential | What it does |\n| --- | --- | --- |\n| Partner API | `platform_api_key` (Bearer) | Screens, ads, analytics, earnings, billing, webhooks |\n| CTV SDK & CSI | `platform_api_key` + `sdk:*` scope | On-glass measurement, occupancy, QR |\n| Sensing | device token | Paired training samples from edge sensing |\n| Device Provisioning | device token | OTA manifests and CCTV pairing |\n| Federated Learning | device fingerprint | Gradient / performance / taste uploads |\n\nAll paths are absolute against `https://api.trillboards.com`.",
    "license": {
      "name": "Proprietary",
      "url": "https://trillboards.com/terms"
    },
    "title": "Trillboards Partner API",
    "version": "1.0.0"
  },
  "openapi": "3.0.3",
  "paths": {
    "/v1/changelog": {
      "get": {
        "description": "Returns structured changelog entries parsed from CHANGELOG-API.md. Filter by date and change type. Unauthenticated, publicly cacheable.",
        "operationId": "queryChangelog",
        "parameters": [
          {
            "description": "Only entries dated on or after this date (YYYY-MM-DD). Unreleased entries always included.",
            "in": "query",
            "name": "since",
            "required": false,
            "schema": {
              "description": "Only entries dated on or after this date (YYYY-MM-DD). Unreleased entries always included.",
              "example": "2026-04-01",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "type": "string"
            }
          },
          {
            "description": "Filter by change category: \"breaking\" (changed+removed), \"additive\" (added), \"deprecation\" (deprecated), \"fix\" (fixed). Comma-separated for multiple.",
            "in": "query",
            "name": "type",
            "required": false,
            "schema": {
              "description": "Filter by change category: \"breaking\" (changed+removed), \"additive\" (added), \"deprecation\" (deprecated), \"fix\" (fixed). Comma-separated for multiple.",
              "example": "breaking",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangelogResponse"
                }
              }
            },
            "description": "List of matching changelog entries."
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "summary": "Query API changelog",
        "tags": [
          "API Meta"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Check what breaking changes were introduced since a given date: GET /v1/changelog?since=2026-04-01&type=breaking",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/errors": {
      "get": {
        "description": "Returns the full error catalog as a JSON list. Each entry includes the stable code string, error type, HTTP status, human description, and doc URL.",
        "operationId": "listErrorCodes",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "code": "insufficient_scope",
                      "description": "The API key is valid but lacks the scope required for this endpoint.",
                      "doc_url": "https://api.trillboards.com/docs/errors#insufficient_scope",
                      "http_status": 403,
                      "type": "permission_error"
                    },
                    {
                      "code": "not_found",
                      "description": "The requested resource does not exist or is not visible to the caller.",
                      "doc_url": "https://api.trillboards.com/docs/errors#not_found",
                      "http_status": 404,
                      "type": "invalid_request_error"
                    }
                  ],
                  "object": "list",
                  "total": 143
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorListResponse"
                }
              }
            },
            "description": "Full error catalog."
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "summary": "List all error codes",
        "tags": [
          "API Meta"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Call this endpoint to enumerate every error code the API can return, so you can build a client-side error handler that covers all cases.",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/errors/{code}": {
      "get": {
        "description": "Returns metadata for a single error code, including an example of the canonical error envelope the API returns when this code is triggered.",
        "operationId": "getErrorCode",
        "parameters": [
          {
            "description": "A stable error code from the catalog. See GET /v1/errors for the full list.",
            "in": "path",
            "name": "code",
            "required": true,
            "schema": {
              "description": "A stable error code from the catalog. See GET /v1/errors for the full list.",
              "example": "not_found",
              "maxLength": 128,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorCodeResponse"
                }
              }
            },
            "description": "Error code details with example response."
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "summary": "Look up a single error code",
        "tags": [
          "API Meta"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Look up a specific error code to understand what it means and see an example response envelope.",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/federated/gradients": {
      "post": {
        "description": "Used by the on-device `FederatedTrainer` to upload top-K-sparsified gradients every ~6 hours. The server buffers gradients per slice (modelType × venueType × venueSubtype × daypart × geo × deviceProfile) and triggers aggregation when the slice buffer reaches the configured threshold. Authentication is device-fingerprint (`X-Device-Fingerprint` header or `screenId` in the body).",
        "operationId": "federatedGradientUpload",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FederatedApi.GradientUploadPayload"
              }
            }
          },
          "description": "Sparsified gradient batch with slice context and attestation envelope.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FederatedApi.GradientUploadResponse"
                }
              }
            },
            "description": "Gradients accepted; response reports current slice buffer state."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "DeviceFingerprint": []
          }
        ],
        "summary": "Upload sparsified gradients for multi-slice federated aggregation.",
        "tags": [
          "Federated Learning"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "federated",
        "x-trillboards-surface": "federated-api"
      }
    },
    "/v1/federated/models/{modelType}": {
      "get": {
        "description": "Returns the most-specific aggregated weights matching the requested slice context. When the exact slice is empty, the server walks up the slice hierarchy (cold-start inheritance). When no parent slice is available, hard-coded defaults are returned with `isDefault=true`.",
        "operationId": "federatedGetModelWeights",
        "parameters": [
          {
            "description": "Federated model name.",
            "in": "path",
            "name": "modelType",
            "required": true,
            "schema": {
              "description": "Federated model name.",
              "maxLength": 64,
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Venue category for slice lookup. Snake_case query alias of venueType.",
            "in": "query",
            "name": "venue_type",
            "required": true,
            "schema": {
              "description": "Venue category for slice lookup. Snake_case query alias of venueType.",
              "maxLength": 64,
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "venue_subtype",
            "required": false,
            "schema": {
              "maxLength": 64,
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "daypart",
            "required": false,
            "schema": {
              "maxLength": 32,
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "geo",
            "required": false,
            "schema": {
              "maxLength": 32,
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "device_profile",
            "required": false,
            "schema": {
              "maxLength": 64,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FederatedApi.ModelInfo"
                }
              }
            },
            "description": "Weights snapshot for the resolved slice."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "summary": "Fetch aggregated weights for the device-specific slice.",
        "tags": [
          "Federated Learning"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "federated",
        "x-trillboards-surface": "federated-api"
      }
    },
    "/v1/federated/models/{modelType}/latest": {
      "get": {
        "description": "Returns metadata describing the latest aggregated model artifact for the requested slice. The client uses `downloadUrl` to pull the weights blob from object storage and `sha256` to verify integrity before applying. Inline `weights` may be returned for lightweight models.",
        "operationId": "federatedGetLatestModelDescriptor",
        "parameters": [
          {
            "description": "Federated model name.",
            "in": "path",
            "name": "modelType",
            "required": true,
            "schema": {
              "description": "Federated model name.",
              "maxLength": 64,
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Venue category (required).",
            "in": "query",
            "name": "venue_type",
            "required": true,
            "schema": {
              "description": "Venue category (required).",
              "maxLength": 64,
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "venue_subtype",
            "required": false,
            "schema": {
              "maxLength": 64,
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "daypart",
            "required": false,
            "schema": {
              "maxLength": 32,
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "geo",
            "required": false,
            "schema": {
              "maxLength": 32,
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "device_profile",
            "required": false,
            "schema": {
              "maxLength": 64,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FederatedApi.ModelLatestResponse"
                }
              }
            },
            "description": "Latest model descriptor (or `available=false` when none exists)."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "summary": "Fetch the latest aggregated model descriptor (download URL + sha256 + version).",
        "tags": [
          "Federated Learning"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "federated",
        "x-trillboards-surface": "federated-api"
      }
    },
    "/v1/federated/performance": {
      "post": {
        "description": "Fire-and-forget INSERT into `federated_model_performance`. Tracks accuracy / latency / anomaly counts per (device, modelType, venueType, modelVersion) so the operator can spot drift before it shows up in revenue.",
        "operationId": "federatedPerformanceUpload",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FederatedApi.PerformanceUploadPayload"
              }
            }
          },
          "description": "Performance datapoint.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FederatedApi.PerformanceUploadResponse"
                }
              }
            },
            "description": "Datapoint queued for persistence."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "DeviceFingerprint": []
          }
        ],
        "summary": "Report on-device model performance metrics.",
        "tags": [
          "Federated Learning"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "federated",
        "x-trillboards-surface": "federated-api"
      }
    },
    "/v1/federated/taste": {
      "post": {
        "description": "Phase 3 endpoint. The device computes a fixed-length screen-taste embedding from accumulated content-impression observations and uploads it for slice aggregation. Embeddings are signed with the device Ed25519 attestation key.",
        "operationId": "federatedTasteUpload",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FederatedApi.TasteUploadPayload"
              }
            }
          },
          "description": "Signed screen-taste embedding with slice context.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FederatedApi.TasteUploadResponse"
                }
              }
            },
            "description": "Embedding accepted into the taste aggregation buffer."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "DeviceFingerprint": []
          }
        ],
        "summary": "Upload a screen-taste embedding for content personalization aggregation.",
        "tags": [
          "Federated Learning"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "federated",
        "x-trillboards-surface": "federated-api"
      }
    },
    "/v1/partner/ads/self-promo": {
      "get": {
        "operationId": "listPartnerSelfPromoAds",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "_id": "65b1c2d3e4f5a6b7c8d9e0f1",
                      "campaign_link": "https://www.example.com/weekend-sale",
                      "createdAt": "2026-08-30T14:11:58.204Z",
                      "instant_ad_qr": false,
                      "name": "Northwind Weekend Sale",
                      "photos": [],
                      "status": "approved",
                      "video": "https://cdn.example.com/partner-self-promo/weekend-sale.mp4"
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/PartnerSelfPromoAd"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Self-promo ads list"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List self-promo ads",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/ads/self-promo\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/ads/self-promo', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/ads/self-promo\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/ads/self-promo');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "operationId": "createPartnerSelfPromoAd",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerSelfPromoCreate"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "_id": "65b1c2d3e4f5a6b7c8d9e0f1",
                    "campaign_link": "https://www.example.com/weekend-sale",
                    "createdAt": "2026-08-30T14:11:58.204Z",
                    "description": "Northwind Weekend Sale",
                    "name": "Northwind Weekend Sale",
                    "partner_id": "6507a1b2c3d4e5f6a7b8cc33",
                    "photos": [],
                    "status": "approved",
                    "status_in_array": [
                      "approved"
                    ],
                    "updatedAt": "2026-08-30T14:11:58.204Z",
                    "userId": "6507a1b2c3d4e5f6a7b8dd44",
                    "video": "https://cdn.example.com/partner-self-promo/weekend-sale.mp4",
                    "video_duration_seconds": 15
                  },
                  "success": true,
                  "warnings": []
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PartnerSelfPromoAd"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "warnings": {
                      "$ref": "#/components/schemas/PartnerCreativeWarnings"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Self-promo ad created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create self-promo ad (URL-based)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/ads/self-promo\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"<name>\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/ads/self-promo', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"name\": \"<name>\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/ads/self-promo\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"name\": \"<name>\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/ads/self-promo');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'name' => '<name>',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/ads/self-promo/placements": {
      "get": {
        "operationId": "listPartnerSelfPromoPlacements",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "ads": [
                        {
                          "advertisement": {
                            "_id": "65b1c2d3e4f5a6b7c8d9e0f1",
                            "name": "Northwind Weekend Sale",
                            "photos": [],
                            "status": "approved",
                            "video": "https://cdn.example.com/partner-self-promo/weekend-sale.mp4"
                          },
                          "allocation_id": "65c2d3e4f5a6b7c8d9e0f1a2",
                          "end_date": "2026-09-30",
                          "start_date": "2026-08-30",
                          "status": "active"
                        }
                      ],
                      "screen": {
                        "_id": "6507a1b2c3d4e5f6a7b8aa11",
                        "name": "Northwind Retail - Entrance"
                      }
                    },
                    {
                      "ads": [],
                      "screen": {
                        "_id": "6507a1b2c3d4e5f6a7b8aa12",
                        "name": "Northwind Retail - Checkout"
                      }
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/PartnerSelfPromoPlacement"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Self-promo placements"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List self-promo placements by screen",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/ads/self-promo/placements\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/ads/self-promo/placements', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/ads/self-promo/placements\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/ads/self-promo/placements');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/ads/self-promo/schedule": {
      "post": {
        "operationId": "schedulePartnerSelfPromo",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "ad_id": {
                    "type": "string"
                  },
                  "end_date": {
                    "format": "date",
                    "type": "string"
                  },
                  "screen_ids": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "start_date": {
                    "format": "date",
                    "type": "string"
                  }
                },
                "required": [
                  "ad_id",
                  "screen_ids"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "success": true
                },
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Self-promo scheduled"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Schedule existing self-promo ad to screens",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/ads/self-promo/schedule\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"ad_id\":\"<ad_id>\",\"screen_ids\":[\"<screen_ids>\"]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/ads/self-promo/schedule', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"ad_id\": \"<ad_id>\",\n    \"screen_ids\": [\n      \"<screen_ids>\"\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/ads/self-promo/schedule\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"ad_id\": \"<ad_id>\",\n        \"screen_ids\": [\n            \"<screen_ids>\",\n        ],\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/ads/self-promo/schedule');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'ad_id' => '<ad_id>',\n        'screen_ids' => [\n            '<screen_ids>',\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/ads/self-promo/upload": {
      "post": {
        "operationId": "createPartnerSelfPromoUpload",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "campaign_link": {
                    "format": "uri",
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "media": {
                    "format": "binary",
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "screen_ids": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "required": [
                  "media",
                  "name"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "_id": "65b1c2d3e4f5a6b7c8d9e0f1",
                    "campaign_link": "https://www.example.com/weekend-sale",
                    "createdAt": "2026-08-30T14:11:58.204Z",
                    "description": "Store Tour",
                    "name": "Store Tour",
                    "partner_id": "6507a1b2c3d4e5f6a7b8cc33",
                    "photos": [],
                    "status": "approved",
                    "status_in_array": [
                      "approved"
                    ],
                    "updatedAt": "2026-08-30T14:11:58.204Z",
                    "userId": "6507a1b2c3d4e5f6a7b8dd44",
                    "video": "https://cdn.example.com/partner-self-promo/store-tour.mp4",
                    "video_duration_seconds": 15
                  },
                  "success": true,
                  "warnings": []
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PartnerSelfPromoAd"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "warnings": {
                      "$ref": "#/components/schemas/PartnerCreativeWarnings"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Self-promo ad created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create self-promo ad (upload)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/ads/self-promo/upload\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -F \"media=@/path/to/media\" \\\n  -F \"name=<name>\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const { openAsBlob } = require('node:fs');\n\nconst form = new FormData();\nform.append('media', await openAsBlob('/path/to/media'));\nform.append('name', \"<name>\");\n\nconst res = await fetch('https://api.trillboards.com/v1/partner/ads/self-promo/upload', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n  body: form,\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/ads/self-promo/upload\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    files={\n        \"media\": open(\"/path/to/media\", \"rb\"),\n    },\n    data={\n        \"name\": \"<name>\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/ads/self-promo/upload');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n    CURLOPT_POSTFIELDS => [\n        'media' => new CURLFile('/path/to/media'),\n        'name' => '<name>',\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/ads/self-promo/{ad_id}/disable": {
      "post": {
        "description": "Marks a self-promo creative as disabled. Disabled self-promos do not\nenter the waterfall and stop accruing impressions. Idempotent: a\nsecond call returns 200 with the same payload.\n",
        "operationId": "disableSelfPromoAd",
        "parameters": [
          {
            "description": "The self-promo creative ID. Note the snake-case path-param spelling.",
            "in": "path",
            "name": "ad_id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ad_id": "65b1c2d3e4f5a6b7c8d9e0f1",
                  "status": "disabled"
                },
                "schema": {
                  "properties": {
                    "ad_id": {
                      "type": "string"
                    },
                    "status": {
                      "enum": [
                        "disabled"
                      ],
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Disabled"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Ad not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Disable a self-promo ad",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/ads/self-promo/<ad_id>/disable\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/ads/self-promo/<ad_id>/disable', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/ads/self-promo/<ad_id>/disable\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/ads/self-promo/<ad_id>/disable');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/agent/register": {
      "post": {
        "description": "Self-service registration for AI agents and automated systems. Returns a\ncentralized platform API key, the exact MCP endpoint, sandbox screens for\ntesting, and free tier information. The same returned key can be supplied\nto `POST /openrtb/v2/onboard` to add buyer capabilities without minting a\nsecond credential.\n\nNo authentication required — designed for fully autonomous agent onboarding.\nRate limited to 5 registrations per hour per IP.\n\nProgrammatic buyer flow:\n```bash\ncurl -sS -X POST https://api.trillboards.com/v1/partner/agent/register \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"agent_type\":\"dsp_agent\",\"name\":\"Example buyer agent\",\"email\":\"programmatic@example.com\",\"company\":\"Example Buyer\"}'\n\ncurl -sS -X POST https://api.trillboards.com/openrtb/v2/onboard \\\n  -H 'Authorization: Bearer <api_key_from_registration>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"company_name\":\"Example Buyer\",\"seat_id\":\"example_buyer\",\"contact_email\":\"programmatic@example.com\"}'\n```\nThe second call links the buyer seat and capabilities to the original key.\nIt does not mint a DSP key.\n",
        "operationId": "registerAgent",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "agent_type": "dsp_agent",
                "company": "Acme DSP",
                "description": "Automated DOOH media buying agent",
                "email": "agent@example.com",
                "name": "My DSP Agent",
                "sandbox": false
              },
              "schema": {
                "properties": {
                  "agent_type": {
                    "description": "Type of agent being registered",
                    "enum": [
                      "dsp_agent",
                      "ssp_agent",
                      "trading_desk",
                      "analytics_platform",
                      "developer"
                    ],
                    "type": "string"
                  },
                  "company": {
                    "description": "Company name (optional)",
                    "type": "string"
                  },
                  "description": {
                    "description": "Brief description of the agent's purpose",
                    "type": "string"
                  },
                  "email": {
                    "description": "Contact email",
                    "example": "agent@example.com",
                    "format": "email",
                    "type": "string"
                  },
                  "name": {
                    "description": "Agent or company name",
                    "example": "My DSP Agent",
                    "type": "string"
                  },
                  "sandbox": {
                    "default": false,
                    "description": "Mint an AdCP **sandbox account** (`account.sandbox` in\n`get_adcp_capabilities`). Media buys made with the returned key\nvalidate end to end — screen resolution, published CPM/floor,\nflight parsing, derived impression goal, every error path — and\nreturn a well-formed response marked `sandbox: true`, but persist\n**no advertisements and no placements**. Nothing can serve,\ndeliver, bill, or pay a venue. Use this for conformance and\nno-spend testing; register without it for a live account.\n",
                    "type": "boolean"
                  }
                },
                "required": [
                  "agent_type",
                  "name",
                  "email"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "account": {
                    "account_id": "652f1c9a4d3b8e0f7a2c5b91",
                    "note": "LIVE account. Media buys made with this key book real, servable, earnings-payable inventory. Register with {\"sandbox\": true} for a test account.",
                    "sandbox": false
                  },
                  "agent_type": "dsp_agent",
                  "api_key": "trb_partner_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                  "available_tools": [
                    "register_device",
                    "get_device_ads",
                    "discover_inventory",
                    "create_media_buy"
                  ],
                  "control_plane_onboarding": {
                    "authorization": "Bearer <same api_key>",
                    "endpoint": "https://api.trillboards.com/openrtb/v2/onboard",
                    "method": "POST",
                    "purpose": "Link this account to a buyer seat and grant DSP/OpenRTB control-plane capabilities.",
                    "reuses_api_key": true
                  },
                  "docs_url": "https://api.trillboards.com/developer",
                  "free_tiers": {
                    "attribution": {
                      "first_campaign": true
                    },
                    "data_api": {
                      "api_calls": 10000
                    },
                    "data_marketplace": {
                      "queries": 5000
                    },
                    "proof_of_play": {
                      "proofs": 1000
                    }
                  },
                  "mcp_endpoint": "https://api.trillboards.com/mcp",
                  "mcp_transport": "streamable-http",
                  "next_steps": [
                    "Use the api_key in Authorization: Bearer <key> header for authenticated endpoints",
                    "Connect to MCP endpoint for AI agent integration",
                    "For buyer access, POST to control_plane_onboarding.endpoint with this same api_key",
                    "Use sandbox screens for testing before going live",
                    "Free tiers apply automatically — no billing setup needed until limits are reached",
                    "When free tier is exceeded, set up billing via POST /v1/partner/billing/setup"
                  ],
                  "object": "agent_registration",
                  "partner_id": "652f1c9a4d3b8e0f7a2c5b91",
                  "pricing_url": "https://api.trillboards.com/v1/partner/pricing",
                  "request_id": "req_9f2c4b8e1d7a63054c9e8f21b3d6a70e",
                  "sandbox": {
                    "note": "Sandbox screens generate simulated audience data for testing.",
                    "screens": [
                      {
                        "device_id": "sandbox_device_4f1a9c02",
                        "name": "Sandbox transit screen"
                      },
                      {
                        "device_id": "sandbox_device_b73e5d18",
                        "name": "Sandbox retail screen"
                      }
                    ]
                  },
                  "tools_url": "https://api.trillboards.com/mcp"
                },
                "schema": {
                  "properties": {
                    "account": {
                      "description": "The AdCP **account** this key is bound to. Distinct from the\n`sandbox` block below, which describes auto-provisioned test\nSCREENS (supply); this is the account-level buying contract\n(demand) declared as `account.sandbox` in\n`get_adcp_capabilities`.\n",
                      "properties": {
                        "account_id": {
                          "description": "Seller-assigned account id. Same value as `partner_id`.",
                          "type": "string"
                        },
                        "note": {
                          "type": "string"
                        },
                        "sandbox": {
                          "description": "True when this is a sandbox account: its media buys write no\nadvertisements and no placements, and every media-buy tool\nresponse is marked `sandbox: true`.\n",
                          "type": "boolean"
                        }
                      },
                      "type": "object"
                    },
                    "agent_type": {
                      "type": "string"
                    },
                    "api_key": {
                      "description": "API key for authentication (display once — save it!)",
                      "example": "trb_partner_xxxxx",
                      "type": "string"
                    },
                    "available_tools": {
                      "description": "MCP tools available to this agent",
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    },
                    "control_plane_onboarding": {
                      "description": "Exact same-key handoff for buyer/OpenRTB control-plane capabilities.",
                      "properties": {
                        "authorization": {
                          "example": "Bearer <same api_key>",
                          "type": "string"
                        },
                        "endpoint": {
                          "example": "https://api.trillboards.com/openrtb/v2/onboard",
                          "type": "string"
                        },
                        "method": {
                          "example": "POST",
                          "type": "string"
                        },
                        "purpose": {
                          "type": "string"
                        },
                        "reuses_api_key": {
                          "example": true,
                          "type": "boolean"
                        }
                      },
                      "type": "object"
                    },
                    "docs_url": {
                      "example": "https://api.trillboards.com/developer",
                      "type": "string"
                    },
                    "free_tiers": {
                      "description": "Free tier limits per product",
                      "type": "object"
                    },
                    "mcp_endpoint": {
                      "example": "https://api.trillboards.com/mcp",
                      "type": "string"
                    },
                    "mcp_transport": {
                      "example": "streamable-http",
                      "type": "string"
                    },
                    "next_steps": {
                      "description": "Guidance for getting started",
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    },
                    "object": {
                      "example": "agent_registration",
                      "type": "string"
                    },
                    "partner_id": {
                      "description": "Unique partner ID",
                      "type": "string"
                    },
                    "pricing_url": {
                      "example": "https://api.trillboards.com/v1/partner/pricing",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "sandbox": {
                      "description": "Auto-provisioned test SCREENS (supply side). Unrelated to `account.sandbox`.",
                      "properties": {
                        "note": {
                          "type": "string"
                        },
                        "screens": {
                          "items": {
                            "properties": {
                              "device_id": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "venue_type": {
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "tools_url": {
                      "example": "https://api.trillboards.com/mcp",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Agent registered successfully"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing required fields or invalid agent_type"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [],
        "summary": "Register an AI agent",
        "tags": [
          "Agent Registration"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/agent/register\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"agent_type\":\"dsp_agent\",\"name\":\"My DSP Agent\",\"email\":\"agent@example.com\",\"company\":\"Acme DSP\",\"description\":\"Automated DOOH media buying agent\",\"sandbox\":false}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/agent/register', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"agent_type\": \"dsp_agent\",\n    \"name\": \"My DSP Agent\",\n    \"email\": \"agent@example.com\",\n    \"company\": \"Acme DSP\",\n    \"description\": \"Automated DOOH media buying agent\",\n    \"sandbox\": false\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/agent/register\",\n    json={\n        \"agent_type\": \"dsp_agent\",\n        \"name\": \"My DSP Agent\",\n        \"email\": \"agent@example.com\",\n        \"company\": \"Acme DSP\",\n        \"description\": \"Automated DOOH media buying agent\",\n        \"sandbox\": False,\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/agent/register');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'agent_type' => 'dsp_agent',\n        'name' => 'My DSP Agent',\n        'email' => 'agent@example.com',\n        'company' => 'Acme DSP',\n        'description' => 'Automated DOOH media buying agent',\n        'sandbox' => false,\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics": {
      "get": {
        "operationId": "getAnalytics",
        "parameters": [
          {
            "in": "query",
            "name": "start_date",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "end_date",
            "schema": {
              "format": "date",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "devices_by_status": [
                      {
                        "_id": "active",
                        "count": 9,
                        "total_impressions": 171204
                      },
                      {
                        "_id": "offline",
                        "count": 3,
                        "total_impressions": 13116
                      }
                    ],
                    "impressions_by_day": [
                      {
                        "_id": "2026-08-24T00:00:00.000Z",
                        "impressions": 25890
                      },
                      {
                        "_id": "2026-08-25T00:00:00.000Z",
                        "impressions": 27110
                      }
                    ],
                    "partner": {
                      "active_devices": 9,
                      "pending_payout_cents": 18450,
                      "total_devices": 12,
                      "total_impressions": 184320,
                      "total_revenue_cents": 246800
                    },
                    "period": {
                      "end": "2026-08-25T00:00:00.000Z",
                      "start": "2026-07-26T00:00:00.000Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "devices_by_status": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "impressions_by_day": {
                          "items": {
                            "properties": {
                              "_id": {
                                "description": "Date (YYYY-MM-DD)",
                                "type": "string"
                              },
                              "impressions": {
                                "type": "integer"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "partner": {
                          "properties": {
                            "active_devices": {
                              "type": "integer"
                            },
                            "pending_payout_cents": {
                              "type": "integer"
                            },
                            "total_devices": {
                              "type": "integer"
                            },
                            "total_impressions": {
                              "type": "integer"
                            },
                            "total_revenue_cents": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "period": {
                          "properties": {
                            "end": {
                              "format": "date-time",
                              "type": "string"
                            },
                            "start": {
                              "format": "date-time",
                              "type": "string"
                            }
                          },
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Analytics data"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get partner analytics",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/audience-continuity": {
      "get": {
        "description": "Returns the top inter-screen audience paths (from-screen / to-screen,\nmedian lag, signals, confidence). Backed by\n`audienceContinuityService.getAudienceContinuity`.\n",
        "operationId": "getAudienceContinuity",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "windowMinutes",
            "schema": {
              "default": 60,
              "maximum": 240,
              "minimum": 5,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attemptedWindows": [
                      "last_7d"
                    ],
                    "confidence": "medium",
                    "dataAsOf": "2026-08-31T14:22:07.481Z",
                    "effectiveTimeRange": "last_7d",
                    "emptyFloorAdvancedFrom": null,
                    "filters": {
                      "creativeId": null,
                      "screenId": null
                    },
                    "gateDiagnostic": {
                      "observedValue": 0,
                      "passed": true
                    },
                    "paths": [
                      {
                        "confidence": "medium",
                        "destinationObservationCount": 361,
                        "destinationScreen": {
                          "id": "65a1b2c3d4e5f6a7b8c9d0e1",
                          "name": "Lobby - South"
                        },
                        "medianLagMinutes": 4.2,
                        "repeatedAnonymousSignalCount": 18,
                        "sampleSize": 132,
                        "sourceObservationCount": 407,
                        "sourceScreen": {
                          "id": "507f1f77bcf86cd799439011",
                          "name": "Lobby - North"
                        },
                        "windowMinutes": 60
                      }
                    ],
                    "requestedTimeRange": "last_7d",
                    "sampleSize": 214,
                    "shortCircuitAdvancedFrom": null,
                    "timeRange": "last_7d",
                    "topPaths": [
                      {
                        "confidence": "medium",
                        "destinationScreenId": "65a1b2c3d4e5f6a7b8c9d0e1",
                        "destinationScreenName": "Lobby - South",
                        "medianLagMinutes": 4.2,
                        "repeatedAnonymousSignalCount": 18,
                        "sourceScreenId": "507f1f77bcf86cd799439011",
                        "sourceScreenName": "Lobby - North"
                      }
                    ],
                    "widenedFromRequested": false,
                    "windowMinutes": 60
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "confidence": {
                          "type": "string"
                        },
                        "sampleSize": {
                          "type": "integer"
                        },
                        "topPaths": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Continuity payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Visitor-flow continuity graph across the partner's screens",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/audience-continuity\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/audience-continuity', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/audience-continuity\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/audience-continuity');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/audience-profile": {
      "get": {
        "description": "Returns the audience-profile composition (demographic segments,\nage/gender distribution, lifestyle bands, confidence scores). Backed\nby `sceneIntelligenceService.getAudienceProfile`.\n",
        "operationId": "getAudienceProfile",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attemptedWindows": [
                      "last_7d"
                    ],
                    "dataAsOf": "2026-08-31T14:22:07.481Z",
                    "effectiveTimeRange": "last_7d",
                    "emptyFloorAdvancedFrom": null,
                    "requestedTimeRange": "last_7d",
                    "segments": [
                      {
                        "count": "12",
                        "income_level": "high",
                        "lifestyle_segment": "foodie"
                      },
                      {
                        "count": "31",
                        "income_level": "medium",
                        "lifestyle_segment": "commuter"
                      }
                    ],
                    "shortCircuitAdvancedFrom": null,
                    "summary": {
                      "total_screens": 12,
                      "venue_distribution": {
                        "gym": 1,
                        "retail": 4,
                        "transit": 7
                      }
                    },
                    "widenedFromRequested": false
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "sampleSize": {
                          "type": "integer"
                        },
                        "segments": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Audience profile payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Audience composition for the partner's screens",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/audience-profile\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/audience-profile', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/audience-profile\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/audience-profile');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/ble-summary": {
      "get": {
        "description": "Returns `{ uniqueDevices, totalObservations }` aggregated across the\npartner's screens for `timeRange`. Backed by the same\n`conversationIntelligenceService.getBleDeviceSummary` the venue\nportal uses.\n",
        "operationId": "getBleSummary",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "totalObservations": 26714,
                    "uniqueDevices": 1843
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "totalObservations": {
                          "type": "integer"
                        },
                        "uniqueDevices": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "BLE summary"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "BLE device discovery summary",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/ble-summary\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/ble-summary', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/ble-summary\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/ble-summary');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/content-performance": {
      "get": {
        "description": "Returns the data shown in the venue-portal \"Content Performance\" modal —\nper-creative ad lineage (advertiser, brand, moderation status, IAB\nclassification, age rating, confidence, served-window) plus measured\naudience reactions (plays, attention, viewers, attentive-time) and the\ncreative-classification chip cloud (subjects, setting, mood, quality).\n\nBacked by the same `audienceMetricsService.getContentPerformance` that\npowers the venue portal — the partner endpoint just scopes the\nscreenIds to the partner's fleet.\n",
        "operationId": "getContentPerformance",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          },
          {
            "description": "`canonical` collapses DSP wrappers under the canonical logical\ncontent (recommended). `wrapper` preserves per-creative_id rows\nfor billing/debug.\n",
            "in": "query",
            "name": "groupBy",
            "schema": {
              "default": "canonical",
              "enum": [
                "canonical",
                "wrapper"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 200,
              "maximum": 500,
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "type": "integer"
            }
          },
          {
            "description": "By default unclassified creatives (advertiserName=NULL / generic\n/ moderation_status=metadata_only) are hidden. Pass `true` to\ninclude them.\n",
            "in": "query",
            "name": "includeUnclassified",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "code": 200,
                  "data": {
                    "accessibleScreens": [],
                    "byScreen": {
                      "507f1f77bcf86cd799439011": {
                        "advertisers": [
                          {
                            "impressions": 14203,
                            "name": "Northwind Retail"
                          },
                          {
                            "impressions": 4118,
                            "name": "Contoso Motors"
                          }
                        ],
                        "screenName": "Northwind Retail - Concourse A"
                      }
                    },
                    "creativeResponse": [
                      {
                        "adId": "65b1c2d3e4f5a6b7c8d9e0f1",
                        "advertiserDomain": "example.com",
                        "advertiserDomains": [
                          "example.com"
                        ],
                        "advertiserName": "Northwind Retail",
                        "ageRating": "G",
                        "analysisKeywords": [
                          "storefront",
                          "autumn",
                          "sale"
                        ],
                        "analysisMood": "upbeat",
                        "analysisQuality": "high",
                        "analysisSetting": "retail_interior",
                        "analysisSubjects": [
                          "people",
                          "products"
                        ],
                        "analysisTextContent": "Autumn Drive - two for one",
                        "assetUrl": "https://cdn.example.com/creatives/autumn-drive-15s.mp4",
                        "attentiveSeconds": 9184.5,
                        "avgAttention": 0.71,
                        "avgBleDwellSeconds": 46.5,
                        "avgFaces": 6.3,
                        "avgReceptivity": 0.44,
                        "brandDisplayName": "Northwind Retail",
                        "brandName": "northwind_retail",
                        "campaignName": "Autumn Drive 2026",
                        "classificationConfidence": 0.92,
                        "classificationReasons": [
                          "brand_domain_match"
                        ],
                        "contentSha256": "3f8a1c0e5b47d29a6c1e8043b7f25de90a4c6b13e7d5820fa96c4e1b3d70a825",
                        "creativeCategory": "retail",
                        "creativeLandingUrl": "https://www.example.com/autumn",
                        "creativeName": "Autumn Drive 15s",
                        "crossScreenReach": 188,
                        "firstSeen": "2026-08-24T09:14:02.000Z",
                        "firstServedAt": "2026-08-24T09:14:02.000Z",
                        "iabCategories": [
                          "IAB22",
                          "IAB22-1"
                        ],
                        "label": "65b1c2d3e4f5a6b7c8d9e0f1 / cr_9182ab",
                        "lastSeen": "2026-08-31T13:47:55.000Z",
                        "lastServedAt": "2026-08-31T13:47:55.000Z",
                        "mediaType": "video",
                        "metricRows": 412,
                        "moderationStatus": "approved",
                        "peakFaces": 14,
                        "playSeconds": 27630,
                        "plays": 1842,
                        "previewUrl": "https://cdn.example.com/creatives/autumn-drive-15s-preview.mp4",
                        "repeatVisitorRate": 0.21,
                        "safetyFlags": [],
                        "sampleCreativeId": "cr_9182ab",
                        "sampleRequestId": "imp_1766644800000_abcd1234",
                        "sceneAdIds": [
                          "65b1c2d3e4f5a6b7c8d9e0f1"
                        ],
                        "sceneSamples": [],
                        "scenesCount": 37,
                        "screenId": "507f1f77bcf86cd799439011",
                        "screenName": "Northwind Retail - Concourse A",
                        "searchMetadata": [],
                        "thumbnailUrl": "https://cdn.example.com/creatives/autumn-drive-15s.jpg",
                        "uniqueCreatives": 1,
                        "uniqueDevicesExposed": 1204
                      }
                    ],
                    "has_more": true,
                    "nextOffset": 25,
                    "topAdvertisers": [
                      {
                        "impressions": 21840,
                        "name": "Northwind Retail"
                      },
                      {
                        "impressions": 9312,
                        "name": "Contoso Motors"
                      }
                    ],
                    "total": 118
                  },
                  "message": "Content performance retrieved",
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "byScreen": {
                          "type": "object"
                        },
                        "creativeResponse": {
                          "items": {
                            "properties": {
                              "adId": {
                                "type": "string"
                              },
                              "advertiserName": {
                                "type": "string"
                              },
                              "ageRating": {
                                "type": "string"
                              },
                              "attentiveSeconds": {
                                "type": "number"
                              },
                              "audioTerms": {
                                "items": {
                                  "type": "string"
                                },
                                "type": "array"
                              },
                              "avgAttention": {
                                "type": "number"
                              },
                              "brandName": {
                                "type": "string"
                              },
                              "confidence": {
                                "type": "number"
                              },
                              "firstSeen": {
                                "format": "date-time",
                                "type": "string"
                              },
                              "iabCategories": {
                                "items": {
                                  "type": "string"
                                },
                                "type": "array"
                              },
                              "lastSeen": {
                                "format": "date-time",
                                "type": "string"
                              },
                              "moderationStatus": {
                                "type": "string"
                              },
                              "plays": {
                                "type": "integer"
                              },
                              "viewers": {
                                "type": "number"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "has_more": {
                          "description": "Whether another page of creativeResponse exists after this one.",
                          "type": "boolean"
                        },
                        "nextOffset": {
                          "nullable": true,
                          "type": "integer"
                        },
                        "topAdvertisers": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "total": {
                          "description": "Number of items the pagination cursor walks — the denominator `nextOffset`/`has_more` are computed against (the filtered set before per-page de-duplication). A page may render fewer rows when visually-identical creatives are merged for display, so page to completion via `has_more` rather than comparing `creativeResponse.length` to `total`. Null on the unfiltered legacy path. Describes the programmatic cursor; the offset-0 all-content block is additive.",
                          "nullable": true,
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Content performance payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Content performance — ad lineage and measured audience reactions",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/content-performance\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/content-performance', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/content-performance\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/content-performance');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/creative-catalog": {
      "get": {
        "description": "Searches the partner's creative catalogue by advertiser name, brand,\ncreative title, or IAB category. Returns lightweight creative\ndescriptors (no audience metrics — use `/analytics/content-performance`\nfor those). Backed by the same `audienceMetricsService.searchCreativeCatalog`\nused by the venue portal.\n",
        "operationId": "searchCreativeCatalog",
        "parameters": [
          {
            "description": "Free-text query. Empty string returns the most-recent creatives.",
            "in": "query",
            "name": "q",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 25,
              "maximum": 50,
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "code": 200,
                  "data": {
                    "creatives": [
                      {
                        "adId": "65b1c2d3e4f5a6b7c8d9e0f1",
                        "advertiserDomain": "example.com",
                        "advertiserDomains": [
                          "example.com"
                        ],
                        "advertiserName": "Northwind Retail",
                        "ageRating": "G",
                        "analysisKeywords": [
                          "storefront",
                          "autumn",
                          "sale"
                        ],
                        "analysisMood": "upbeat",
                        "analysisQuality": "high",
                        "analysisSetting": "retail_interior",
                        "analysisSubjects": [
                          "people",
                          "products"
                        ],
                        "analysisTextContent": "Autumn Drive - two for one",
                        "assetUrl": "https://cdn.example.com/creatives/autumn-drive-15s.mp4",
                        "attentiveSeconds": 0,
                        "avgAttention": 0,
                        "avgFaces": 0,
                        "avgReceptivity": 0,
                        "brandDisplayName": "Northwind Retail",
                        "brandName": "northwind_retail",
                        "campaignName": "Autumn Drive 2026",
                        "classificationConfidence": 0.92,
                        "classificationReasons": [
                          "brand_domain_match"
                        ],
                        "creativeCategory": "retail",
                        "creativeLandingUrl": "https://www.example.com/autumn",
                        "creativeName": "Autumn Drive 15s",
                        "firstSeen": "2026-08-24T09:14:02.000Z",
                        "firstServedAt": "2026-08-24T09:14:02.000Z",
                        "iabCategories": [
                          "IAB22",
                          "IAB22-1"
                        ],
                        "label": "65b1c2d3e4f5a6b7c8d9e0f1 / cr_9182ab",
                        "lastSeen": "2026-08-31T13:47:55.000Z",
                        "lastServedAt": "2026-08-31T13:47:55.000Z",
                        "mediaType": "video",
                        "metricRows": 0,
                        "moderationStatus": "approved",
                        "peakFaces": 0,
                        "playSeconds": 105,
                        "plays": 7,
                        "previewUrl": "https://cdn.example.com/creatives/autumn-drive-15s-preview.mp4",
                        "safetyFlags": [],
                        "sampleCreativeId": "cr_9182ab",
                        "sceneSamples": [],
                        "screenId": "507f1f77bcf86cd799439011",
                        "screenName": "Northwind Retail - Concourse A",
                        "searchMetadata": [],
                        "thumbnailUrl": "https://cdn.example.com/creatives/autumn-drive-15s.jpg",
                        "uniqueCreatives": 1
                      }
                    ],
                    "has_more": true,
                    "nextOffset": 25,
                    "total": null
                  },
                  "message": "Creative catalog retrieved",
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "creatives": {
                          "items": {
                            "properties": {
                              "adId": {
                                "type": "string"
                              },
                              "advertiserName": {
                                "type": "string"
                              },
                              "assetUrl": {
                                "format": "uri",
                                "nullable": true,
                                "type": "string"
                              },
                              "brandName": {
                                "type": "string"
                              },
                              "iabCategories": {
                                "items": {
                                  "type": "string"
                                },
                                "type": "array"
                              },
                              "moderationStatus": {
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "has_more": {
                          "description": "Whether another page of results exists after this one.",
                          "type": "boolean"
                        },
                        "nextOffset": {
                          "nullable": true,
                          "type": "integer"
                        },
                        "total": {
                          "description": "Always null for catalogue search: the read over-fetches limit+1 for a free `has_more`, but an exact total would need a second COUNT round-trip the search path deliberately avoids. Page to completion via `has_more`.",
                          "nullable": true,
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Creative catalogue results"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Free-text search across the partner's creative catalogue",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/creative-catalog\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/creative-catalog', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/creative-catalog\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/creative-catalog');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/creative-scenes": {
      "get": {
        "description": "Returns the timestamped scene observations captured while a specific\ncreative (`adId`) was playing on a specific screen (`screenId`). Each\nscene includes the audience-class chip cloud, attention metrics,\nengagement narrative, and the cloud-emitted `ad_fit_verdict`. Cursor-\npaginated. Backed by `audienceMetricsService.getCreativeSceneObservations`.\n",
        "operationId": "getCreativeScenes",
        "parameters": [
          {
            "in": "query",
            "name": "adId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Must belong to the partner's accessible screens.",
            "in": "query",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          },
          {
            "description": "Opaque pagination cursor returned by the previous response.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 100,
              "maximum": 200,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "adId": "65b1c2d3e4f5a6b7c8d9e0f1",
                    "nextCursor": "2026-08-31T13:47:55.000Z|A busy hotel lobby with guests checking in",
                    "scenes": [
                      {
                        "adReceptivity": 0.44,
                        "avgAttention": 0.71,
                        "avgFaces": 6.3,
                        "contentSha256": "3f8a1c0e5b47d29a6c1e8043b7f25de90a4c6b13e7d5820fa96c4e1b3d70a825",
                        "contextualRelevance": "0.85",
                        "creativeId": "cr_9182ab",
                        "observedAt": "2026-08-31T13:47:55.000Z",
                        "playCompletedAt": "2026-08-31T13:47:55.000Z",
                        "playStartedAt": "2026-08-31T13:47:40.000Z",
                        "sceneDescription": "A busy hotel lobby with guests checking in"
                      }
                    ],
                    "screenId": "507f1f77bcf86cd799439011",
                    "timeRange": "last_7d",
                    "total": 37
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "adId": {
                          "type": "string"
                        },
                        "nextCursor": {
                          "nullable": true,
                          "type": "string"
                        },
                        "scenes": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "screenId": {
                          "type": "string"
                        },
                        "timeRange": {
                          "type": "string"
                        },
                        "total": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Scene observations"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing `adId` or `screenId`"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "`screenId` not accessible to this partner"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Scene-level observations for a played creative",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/creative-scenes?adId=%3CadId%3E&screenId=%3CscreenId%3E\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/creative-scenes?adId=%3CadId%3E&screenId=%3CscreenId%3E', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/creative-scenes?adId=%3CadId%3E&screenId=%3CscreenId%3E\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/creative-scenes?adId=%3CadId%3E&screenId=%3CscreenId%3E');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/cross-screen-flow": {
      "get": {
        "description": "Returns the directional flow between every pair of partner screens\n(Sankey-friendly). Includes `edges` (from, to, flow_count, weight)\nand `screens` (position lookup). Backed by\n`crossScreenFlowService.getCrossScreenFlow`.\n",
        "operationId": "getCrossScreenFlow",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attemptedWindows": [
                      "last_7d"
                    ],
                    "dataAsOf": "2026-08-31T14:22:07.481Z",
                    "dataThrough": "2026-08-30T00:00:00.000Z",
                    "distanceMeters": null,
                    "effectiveTimeRange": "last_7d",
                    "emptyFloorAdvancedFrom": null,
                    "flows": [
                      {
                        "asOf": "2026-08-30T00:00:00.000Z",
                        "coVisitDevices": 6,
                        "destinationId": "65a1b2c3d4e5f6a7b8c9d0e1",
                        "deviceCount": 8,
                        "sourceId": "507f1f77bcf86cd799439011",
                        "transitionDistanceM": 14
                      },
                      {
                        "asOf": "2026-08-29T00:00:00.000Z",
                        "coVisitDevices": 6,
                        "destinationId": "507f1f77bcf86cd799439011",
                        "deviceCount": 5,
                        "sourceId": "65a1b2c3d4e5f6a7b8c9d0e1",
                        "transitionDistanceM": 14
                      }
                    ],
                    "flowsByDay": {
                      "crossDay": {
                        "flows": [],
                        "visitedBoth": 38
                      },
                      "sameDay": {
                        "flows": [],
                        "visitedBoth": 174
                      }
                    },
                    "focusedPair": null,
                    "gateDiagnostic": {
                      "observedValue": 2,
                      "passed": true,
                      "reason": "ok"
                    },
                    "globalReach": {
                      "regulars": 141,
                      "saw2PlusScreens": 212,
                      "uniqueDevices": 2832
                    },
                    "journeys": {
                      "crossScreenDevices": 212,
                      "crossScreenRepeatRate": 0,
                      "paths": [],
                      "recallLift": 0,
                      "singleScreenDevices": 2620,
                      "singleScreenRepeatRate": 0
                    },
                    "onlyAtA": 1628,
                    "onlyAtB": 992,
                    "repeatVisitorShare": 12.44,
                    "requestedTimeRange": "last_7d",
                    "screens": [
                      {
                        "dwell": 46.5,
                        "id": "507f1f77bcf86cd799439011",
                        "latitude": 37.7752,
                        "longitude": -122.4148,
                        "name": "Lobby",
                        "reach": 1840,
                        "sharedReach": 212,
                        "venueType": "transit"
                      },
                      {
                        "dwell": 88.2,
                        "id": "65a1b2c3d4e5f6a7b8c9d0e1",
                        "latitude": 37.7753,
                        "longitude": -122.4147,
                        "name": "Bar",
                        "reach": 1204,
                        "sharedReach": 212,
                        "venueType": "bar"
                      }
                    ],
                    "shortCircuitAdvancedFrom": null,
                    "topCrossCreative": null,
                    "visitedBoth": 212,
                    "widenedFromRequested": false
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "edges": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "screens": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Cross-screen flow payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Per-pair flow directionality + transition heatmap",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/cross-screen-flow\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/cross-screen-flow', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/cross-screen-flow\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/cross-screen-flow');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/dashboard": {
      "get": {
        "description": "Returns the same unified dashboard payload the venue portal renders\nat `/v2/earner/analytics/dashboard`, scoped to the partner's fleet.\nDelegates to `earnerAnalyticsService.getDashboard` via the\npartner-context `screenIdsOverride` path so the shared service skips\nthe owner/manager lookup and operates on the pre-authorized screen\nlist. Earnings figures reflect the partner's revenue share where\napplicable.\n",
        "operationId": "getPartnerAnalyticsDashboard",
        "parameters": [
          {
            "in": "query",
            "name": "period",
            "required": false,
            "schema": {
              "default": "7d",
              "enum": [
                "7d",
                "30d",
                "90d",
                "custom"
              ],
              "type": "string"
            }
          },
          {
            "description": "ISO date (required when period=custom)",
            "in": "query",
            "name": "startDate",
            "required": false,
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "ISO date (required when period=custom)",
            "in": "query",
            "name": "endDate",
            "required": false,
            "schema": {
              "format": "date",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "ads": {
                      "change": {
                        "absolute": 3566,
                        "direction": "up",
                        "percentage": 17
                      },
                      "current": {
                        "completionRate": 96,
                        "played": 24610
                      },
                      "previous": {
                        "completionRate": 95,
                        "played": 21044
                      }
                    },
                    "audience": {
                      "change": {
                        "absolute": 3566,
                        "direction": "up",
                        "percentage": 17
                      },
                      "current": {
                        "avgDwellSeconds": 0,
                        "viewers": 24610
                      },
                      "previous": {
                        "avgDwellSeconds": 0,
                        "viewers": 21044
                      }
                    },
                    "earnings": {
                      "change": {
                        "absolute": 32290,
                        "direction": "up",
                        "percentage": 21
                      },
                      "current": {
                        "direct": 43500,
                        "programmatic": 140770,
                        "total": 184270
                      },
                      "previous": {
                        "direct": 39000,
                        "programmatic": 112980,
                        "total": 151980
                      },
                      "sparkline": [
                        {
                          "date": "2026-08-25T00:00:00.000Z",
                          "value": 18420
                        },
                        {
                          "date": "2026-08-26T00:00:00.000Z",
                          "value": 21077
                        },
                        {
                          "date": "2026-08-27T00:00:00.000Z",
                          "value": 0
                        }
                      ]
                    },
                    "insights": [
                      {
                        "actionUrl": "/earner/screens/65a1b2c3d4e5f6a7b8c9d0e2",
                        "message": "\"Northwind Retail — Concourse B\" has been offline for 16 hours",
                        "screenId": "65a1b2c3d4e5f6a7b8c9d0e2",
                        "type": "warning"
                      },
                      {
                        "message": "Programmatic ads now 76% of your earnings (was 74% last period)",
                        "type": "info"
                      }
                    ],
                    "period": {
                      "endDate": "2026-08-31T00:00:00.000Z",
                      "label": "Last 7 days",
                      "startDate": "2026-08-25T00:00:00.000Z"
                    },
                    "screens": {
                      "needsAttention": [
                        {
                          "id": "65a1b2c3d4e5f6a7b8c9d0e2",
                          "name": "Northwind Retail — Concourse B",
                          "offlineSince": "2026-08-30T22:11:04.000Z"
                        }
                      ],
                      "offline": 1,
                      "online": 11,
                      "total": 12
                    },
                    "topScreens": [
                      {
                        "attention": 61,
                        "change": {
                          "absolute": 24110,
                          "direction": "up",
                          "percentage": 22
                        },
                        "earnings": 131220,
                        "hours": 18.5,
                        "id": "507f1f77bcf86cd799439011",
                        "isDeleted": false,
                        "location": "1 Market Plaza, Springfield",
                        "name": "Northwind Retail — Concourse A",
                        "plays": 18420,
                        "status": "online",
                        "viewers": 18420
                      }
                    ]
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "ads": {
                          "type": "object"
                        },
                        "audience": {
                          "type": "object"
                        },
                        "earnings": {
                          "properties": {
                            "change": {
                              "type": "object"
                            },
                            "current": {
                              "type": "object"
                            },
                            "previous": {
                              "type": "object"
                            },
                            "sparkline": {
                              "items": {
                                "type": "object"
                              },
                              "type": "array"
                            }
                          },
                          "type": "object"
                        },
                        "insights": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "period": {
                          "properties": {
                            "endDate": {
                              "nullable": true,
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            },
                            "startDate": {
                              "nullable": true,
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "screens": {
                          "type": "object"
                        },
                        "topScreens": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Dashboard payload"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Custom period requires startDate and endDate"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Unified portfolio dashboard (earnings, ads, audience, screens, insights)",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/dashboard\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/dashboard', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/dashboard\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/dashboard');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/device-suite-signature": {
      "get": {
        "description": "Returns the device-class breakdown observed across the partner's\nscreens — useful for ad-creative format/codec decisions and\npartner-audience targeting. Backed by\n`deviceSuiteSignatureService.getDeviceSuiteSignature`.\n",
        "operationId": "getDeviceSuiteSignature",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attemptedWindows": [
                      "last_7d"
                    ],
                    "ble_coverage": true,
                    "buckets": [
                      {
                        "confidence_mix": {
                          "high": 0,
                          "low": 0,
                          "medium": 3
                        },
                        "description": "Personal smartphone (BLE personal-device signal)",
                        "devices": 3,
                        "label": "Phone",
                        "pct": 0.5,
                        "type": "phone"
                      },
                      {
                        "confidence_mix": {
                          "high": 2,
                          "low": 0,
                          "medium": 0
                        },
                        "description": "Watch, earbuds or fitness band",
                        "devices": 2,
                        "label": "Wearable",
                        "pct": 0.3333,
                        "type": "wearable"
                      },
                      {
                        "confidence_mix": {
                          "high": 0,
                          "low": 1,
                          "medium": 0
                        },
                        "description": "Signals insufficient to identify a device type",
                        "devices": 1,
                        "label": "Unknown",
                        "pct": 0.1667,
                        "type": "unknown"
                      }
                    ],
                    "dataAsOf": "2026-08-31T14:22:07.481Z",
                    "effectiveTimeRange": "last_7d",
                    "emptyFloorAdvancedFrom": null,
                    "gateDiagnostic": {
                      "observedValue": 6,
                      "passed": true
                    },
                    "recencyBreakdown": {
                      "newToday": 2,
                      "regulars": 0,
                      "returning30d": 1,
                      "returning7d": 3
                    },
                    "requestedTimeRange": "last_7d",
                    "shortCircuitAdvancedFrom": null,
                    "typeTaxonomy": [
                      {
                        "description": "Personal smartphone (BLE personal-device signal)",
                        "label": "Phone",
                        "type": "phone"
                      },
                      {
                        "description": "Watch, earbuds or fitness band",
                        "label": "Wearable",
                        "type": "wearable"
                      },
                      {
                        "description": "Personal computer",
                        "label": "Laptop",
                        "type": "laptop"
                      },
                      {
                        "description": "Tablet device",
                        "label": "Tablet",
                        "type": "tablet"
                      },
                      {
                        "description": "Smart TV, Apple TV or media streamer",
                        "label": "TV / Streaming",
                        "type": "tv"
                      },
                      {
                        "description": "Smart speaker or audio device",
                        "label": "Speaker",
                        "type": "speaker"
                      },
                      {
                        "description": "Tag, peripheral or IoT accessory",
                        "label": "Accessory",
                        "type": "iot_accessory"
                      },
                      {
                        "description": "Network or Bluetooth printer",
                        "label": "Printer",
                        "type": "printer"
                      },
                      {
                        "description": "Signals insufficient to identify a device type",
                        "label": "Unknown",
                        "type": "unknown"
                      }
                    ],
                    "vendorMix": {
                      "buckets": [
                        {
                          "devices": 4,
                          "pct": 0.6667,
                          "vendor": "Apple",
                          "vendorId": 76
                        },
                        {
                          "devices": 2,
                          "pct": 0.3333,
                          "vendor": "Samsung",
                          "vendorId": 117
                        }
                      ]
                    },
                    "visitFrequencyDistribution": [
                      {
                        "bucket": "1",
                        "devices": 4,
                        "pct": 0.6667
                      },
                      {
                        "bucket": "2-5",
                        "devices": 2,
                        "pct": 0.3333
                      },
                      {
                        "bucket": "6+",
                        "devices": 0,
                        "pct": 0
                      }
                    ],
                    "widenedFromRequested": false
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "deviceTypes": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "totalDevices": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Device-suite signature"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Device-type / OS / browser-class distribution among repeat visitors",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/device-suite-signature\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/device-suite-signature', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/device-suite-signature\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/device-suite-signature');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/export": {
      "get": {
        "description": "Mirrors the venue-portal earner export at\n`/v2/earner/analytics/export`, scoped to the partner's fleet via\nthe `screenIdsOverride` path through\n`earnerAnalyticsService.getExportData`. Response body is the CSV\ntext directly; `Content-Disposition: attachment` is set so a\nbrowser will download instead of rendering.\n",
        "operationId": "exportPartnerAnalytics",
        "parameters": [
          {
            "in": "query",
            "name": "period",
            "required": false,
            "schema": {
              "default": "30d",
              "enum": [
                "7d",
                "30d",
                "90d",
                "custom"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "type",
            "required": false,
            "schema": {
              "default": "screen-breakdown",
              "enum": [
                "summary",
                "daily-breakdown",
                "screen-breakdown"
              ],
              "type": "string"
            }
          },
          {
            "description": "ISO date (required when period=custom)",
            "in": "query",
            "name": "startDate",
            "required": false,
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "ISO date (required when period=custom)",
            "in": "query",
            "name": "endDate",
            "required": false,
            "schema": {
              "format": "date",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "CSV download"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Custom period requires startDate and endDate"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "CSV export of partner-scope analytics (summary / daily / per-screen)",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "# responds with text/csv\ncurl -sS \"https://api.trillboards.com/v1/partner/analytics/export\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/export', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.text(); // text/csv\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/export\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.text  # text/csv\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/export');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n// $response is text/csv — do not json_decode it.\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/export/ads-by-day": {
      "get": {
        "description": "Returns one row per (date, screen, ad) for the partner's fleet\nacross the supplied date range. Reads from the live\n`ScreenDailyAnalyticsByAd` store scoped to the partner's screen\nlist (the aggregation cron is still Mongo-only; reading the PG\nmirror would silently empty recent days). Default response is\nCSV; `format=json` returns the structured rows for programmatic\nconsumption.\n",
        "operationId": "exportPartnerAdsByDay",
        "parameters": [
          {
            "description": "ISO date (YYYY-MM-DD)",
            "in": "query",
            "name": "startDate",
            "required": true,
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "ISO date (YYYY-MM-DD)",
            "in": "query",
            "name": "endDate",
            "required": true,
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "format",
            "required": false,
            "schema": {
              "default": "csv",
              "enum": [
                "csv",
                "json"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "_id": "d3a1f0c2-8b54-4e19-9f77-2c6a5b0e41d8",
                      "ad_name": "Autumn Drive 15s",
                      "advertiser_name": "Northwind Retail",
                      "campaign_type": "programmatic",
                      "completion_rate": 96.4,
                      "date": "2026-08-30T00:00:00.000Z",
                      "gross_revenue_cents": 21870,
                      "plays_completed": 1842,
                      "publisher_revenue_cents": 13122,
                      "publisher_share_rate": 0.6,
                      "screen_name": "Northwind Retail - Concourse A"
                    },
                    {
                      "_id": "5f7c9b10-2e44-4a03-b8d6-70915ca3ee62",
                      "ad_name": "Store Opening 10s",
                      "advertiser_name": "Northwind Retail",
                      "campaign_type": "direct",
                      "completion_rate": 99.01,
                      "date": "2026-08-30T00:00:00.000Z",
                      "gross_revenue_cents": 7250,
                      "plays_completed": 604,
                      "publisher_revenue_cents": 4350,
                      "publisher_share_rate": 0.6,
                      "screen_name": "Northwind Retail - Concourse B"
                    }
                  ],
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "properties": {
                          "ad_name": {
                            "type": "string"
                          },
                          "advertiser_name": {
                            "type": "string"
                          },
                          "campaign_type": {
                            "type": "string"
                          },
                          "completion_rate": {
                            "type": "number"
                          },
                          "date": {
                            "type": "string"
                          },
                          "gross_revenue_cents": {
                            "type": "integer"
                          },
                          "plays_completed": {
                            "type": "integer"
                          },
                          "publisher_revenue_cents": {
                            "type": "integer"
                          },
                          "publisher_share_rate": {
                            "type": "number"
                          },
                          "screen_name": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "CSV download (or JSON when format=json)"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "startDate and endDate are required"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Per-ad daily breakdown for the partner fleet",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/export/ads-by-day?startDate=%3CstartDate%3E&endDate=%3CendDate%3E\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/export/ads-by-day?startDate=%3CstartDate%3E&endDate=%3CendDate%3E', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/export/ads-by-day?startDate=%3CstartDate%3E&endDate=%3CendDate%3E\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/export/ads-by-day?startDate=%3CstartDate%3E&endDate=%3CendDate%3E');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/fleet-hourly": {
      "get": {
        "description": "Returns hourly visitor counts and average attention for each hour of\nthe day, aggregated across the partner's fleet for `timeRange`.\nBacked by `audienceMetricsService.getFleetHourlyBreakdown`.\n",
        "operationId": "getFleetHourly",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "today",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "avgAttention": 0.5,
                      "avgEngagement": 0.5,
                      "avgReceptivity": 0.4,
                      "avgViewers": 5,
                      "dominantIncome": "high",
                      "dominantMood": "curious",
                      "hour": 9,
                      "measurements": 12,
                      "peakViewers": 8
                    },
                    {
                      "avgAttention": 0.6,
                      "avgEngagement": 0.6,
                      "avgReceptivity": 0.5,
                      "avgViewers": 9,
                      "dominantIncome": "premium",
                      "dominantMood": "engaged",
                      "hour": 17,
                      "measurements": 30,
                      "peakViewers": 15
                    }
                  ],
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "description": "One entry per hour of the day. A partner with no screens gets the empty\nform `{ hours: [], timeRange }` instead of an array.\n",
                      "oneOf": [
                        {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        {
                          "properties": {
                            "hours": {
                              "items": {
                                "type": "object"
                              },
                              "type": "array"
                            },
                            "timeRange": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        }
                      ]
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Fleet hourly payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "24-hour distribution across the partner's fleet",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/fleet-hourly\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/fleet-hourly', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/fleet-hourly\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/fleet-hourly');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/halo-funnel": {
      "get": {
        "description": "Returns the partner-fleet funnel measured from venue audience\nsignals: stage counts, drop-off ratios, conversion signals.\nBacked by `haloFunnelService.getHaloFunnel`.\n",
        "operationId": "getHaloFunnel",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attemptedWindows": [
                      "last_7d"
                    ],
                    "dataAsOf": "2026-08-31T14:22:07.481Z",
                    "distanceBands": {
                      "atScreen": 214,
                      "nearby": 703,
                      "passing": 363,
                      "total": 1280
                    },
                    "effectiveTimeRange": "last_7d",
                    "emptyFloorAdvancedFrom": null,
                    "gateDiagnostic": {
                      "observedValue": 11,
                      "passed": true
                    },
                    "requestedTimeRange": "last_7d",
                    "shortCircuitAdvancedFrom": null,
                    "tiers": [
                      {
                        "count": 1280,
                        "key": "in_range"
                      },
                      {
                        "count": 320,
                        "key": "face_co_observed"
                      },
                      {
                        "count": 64,
                        "key": "attended"
                      },
                      {
                        "count": 11,
                        "key": "deep_dwell"
                      },
                      {
                        "count": 9,
                        "key": "returning"
                      },
                      {
                        "count": 7,
                        "key": "cross_screen"
                      }
                    ],
                    "widenedFromRequested": false
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "stages": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Halo funnel payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "4-stage discovery → awareness → engagement → conversion funnel",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/halo-funnel\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/halo-funnel', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/halo-funnel\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/halo-funnel');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/loyalty-clock": {
      "get": {
        "description": "Returns the 24-hour radial showing the share of returning vs first-\ntimer audience per hour over the `timeRange` window. \"Returning\" is\ndefined as the same resolved_device_id_hash seen on a different\ncalendar day within the lookback window. Backed by\n`loyaltyClockService.getLoyaltyClock`.\n",
        "operationId": "getLoyaltyClock",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "buckets": [
                      {
                        "avgUniqueDevicesPerDay": 0,
                        "hour": 0,
                        "observations": 0,
                        "returning": 0,
                        "returningDevices": 0,
                        "returningPct": 0,
                        "total": 0,
                        "uniqueDevices": 0
                      },
                      {
                        "avgUniqueDevicesPerDay": 174,
                        "hour": 9,
                        "observations": 4832,
                        "returning": 137,
                        "returningDevices": 137,
                        "returningPct": 0.1122,
                        "total": 1221,
                        "uniqueDevices": 1221
                      },
                      {
                        "avgUniqueDevicesPerDay": 292,
                        "hour": 18,
                        "observations": 8710,
                        "returning": 361,
                        "returningDevices": 361,
                        "returningPct": 0.1766,
                        "total": 2044,
                        "uniqueDevices": 2044
                      }
                    ],
                    "gateDiagnostic": {
                      "observedValue": 3265,
                      "passed": true
                    },
                    "venueLoyalty": {
                      "available": true,
                      "loyalDevices": 486,
                      "returningDevices": 1743,
                      "returningPct": 0.2143,
                      "tierBreakdown": {
                        "drive_by": 6391,
                        "loyalist": 95,
                        "occasional": 1257,
                        "regular": 391
                      },
                      "topDaypart": "evening",
                      "totalDevices": 8134
                    }
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "hours": {
                          "items": {
                            "properties": {
                              "firstTimerShare": {
                                "type": "number"
                              },
                              "hour": {
                                "maximum": 23,
                                "minimum": 0,
                                "type": "integer"
                              },
                              "returningShare": {
                                "type": "number"
                              },
                              "sampleSize": {
                                "type": "integer"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "sampleSize": {
                          "type": "integer"
                        },
                        "timeRange": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Loyalty clock payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "24-hour returning-vs-first-timer audience clock",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/loyalty-clock\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/loyalty-clock', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/loyalty-clock\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/loyalty-clock');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/network-summary": {
      "get": {
        "description": "Returns the fleet-wide audience summary for the partner's screens\nfor the given `timeRange`. Equivalent to the non-live path of\n`GET /v2/earner/analytics/network-summary`. Backed by\n`audienceMetricsService.getNetworkSummaryByScreenIds`. The\nWebSocket-style live mode is not exposed to partners via this\nendpoint; partners poll the standard rollup.\n",
        "operationId": "getNetworkSummary",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "today",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "activeScreensWithData": 11,
                    "avgAttentionScore": 0.68,
                    "avgCrowdedPct": 0.18,
                    "avgDwellTime": 5000,
                    "avgEmotionalEngagement": 0.63,
                    "avgPasserbyRatio": 0.72,
                    "avgViewersPerScreen": 6.3,
                    "bodyLanguage": {
                      "audienceReaction": {
                        "DISINTERESTED": 42,
                        "HIGHLY_ENGAGED": 164,
                        "INTERESTED": 936,
                        "NEGATIVE": 920,
                        "NEUTRAL": 1716
                      },
                      "audienceReactionSampleCount": 3778,
                      "avgEmotionalEngagement": 0.63,
                      "facing": "glancing",
                      "mood": "neutral",
                      "movementPace": "stationary",
                      "reaction": {
                        "DISINTERESTED": 42,
                        "HIGHLY_ENGAGED": 164,
                        "INTERESTED": 936,
                        "NEGATIVE": 920,
                        "NEUTRAL": 1716
                      }
                    },
                    "cameraAdoptionRate": 0.75,
                    "cameraEnabledScreens": 9,
                    "dataAvailable": true,
                    "demographics": {
                      "ageDistribution": {
                        "25-34": 40,
                        "35-44": 15
                      },
                      "genderSplit": {
                        "female": 30,
                        "male": 25
                      }
                    },
                    "dominantEngagement": "engaged",
                    "dominantMood": "happy",
                    "dominantPace": "stationary",
                    "incomeDistribution": {
                      "high": 100,
                      "low": 0,
                      "medium": 0,
                      "premium": 0
                    },
                    "lifestyleDistribution": [
                      {
                        "percentage": 42.1,
                        "segment": "Professional"
                      },
                      {
                        "percentage": 31.7,
                        "segment": "Commuter"
                      }
                    ],
                    "liveViewerData": [
                      {
                        "attention": 0.71,
                        "currentViewers": 8,
                        "incomeLevel": "high",
                        "lastUpdated": "2026-08-31T14:22:07.481Z",
                        "maxViewers": 12,
                        "mood": "happy",
                        "screenId": "507f1f77bcf86cd799439011"
                      }
                    ],
                    "liveViewers": 34,
                    "peakViewers": 41,
                    "purchaseIntent": {
                      "dominantBagTier": null,
                      "shoppingRate": 0.12
                    },
                    "queriedAt": "2026-08-31T14:22:07.481Z",
                    "screensWithLiveViewers": 7,
                    "timeRange": "today",
                    "topPerformers": [
                      {
                        "avgAttention": 0.71,
                        "avgViewers": 8,
                        "dominantIncome": "high",
                        "location": "1 Market Plaza, Springfield",
                        "name": "Northwind Retail - Concourse A",
                        "screenId": "507f1f77bcf86cd799439011"
                      }
                    ],
                    "totalFootTraffic": 41870,
                    "totalMeasurements": 184362,
                    "totalScreens": 12,
                    "totalShoppingBags": 0,
                    "totalViewers": 24610
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "dataAvailable": {
                          "type": "boolean"
                        },
                        "queriedAt": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "timeRange": {
                          "type": "string"
                        },
                        "totalScreens": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Network audience summary"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Fleet-wide audience summary for the partner's screens",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/network-summary\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/network-summary', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/network-summary\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/network-summary');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/network-trend": {
      "get": {
        "description": "Returns a timeseries of fleet-wide visitor counts + average attention\nover `timeRange`, bucketed by `interval`. Backed by\n`audienceMetricsService.getNetworkTrend`.\n",
        "operationId": "getNetworkTrend",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "interval",
            "schema": {
              "default": "1d",
              "enum": [
                "1h",
                "1d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "avgAttention": 0.5,
                      "avgReceptivity": 0.4,
                      "date": "2026-08-30T00:00:00.000Z",
                      "totalSamples": 10,
                      "totalViewers": 100
                    },
                    {
                      "avgAttention": 0.6,
                      "avgReceptivity": 0.45,
                      "date": "2026-08-31T00:00:00.000Z",
                      "totalSamples": 20,
                      "totalViewers": 200
                    }
                  ],
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "description": "One bucket per `interval`. A partner with no screens gets the empty\nform `{ series: [], interval, timeRange }` instead of an array.\n",
                      "oneOf": [
                        {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        {
                          "properties": {
                            "interval": {
                              "type": "string"
                            },
                            "series": {
                              "items": {
                                "type": "object"
                              },
                              "type": "array"
                            },
                            "timeRange": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        }
                      ]
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Network trend payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Daily/hourly fleet-wide audience timeseries",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/network-trend\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/network-trend', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/network-trend\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/network-trend');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/proximity-summary": {
      "get": {
        "description": "Returns the same proximity-graph summary the venue-portal Insights\npane shows — `uniqueSignals`, `repeatSignals`, `crossScreenSignals`,\nper-source breakdown (BLE / WiFi / mDNS / gateway), distance-aware\ngate diagnostics, continuity edges, and top paths. Backed by\n`conversationIntelligenceService.getProximitySummary`. Every count is\nan aggregate over the partner's screen set.\n",
        "operationId": "getProximitySummary",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attemptedWindows": [
                      "last_7d"
                    ],
                    "avgUniqueDevicesPerDay": 3067,
                    "crossScreenAmplificationPct": 0.0731,
                    "crossScreenSignals": 942,
                    "dataAsOf": "2026-08-31T14:22:07.481Z",
                    "effectiveTimeRange": "last_7d",
                    "emptyFloorAdvancedFrom": null,
                    "gateDiagnostic": {
                      "observedValue": 942,
                      "passed": true
                    },
                    "householdsInferred": 0,
                    "lagP50Minutes": 27.5,
                    "regularSharePct": 13.88,
                    "regularsCount": 1129,
                    "repeatSignals": 6318,
                    "requestedTimeRange": "last_7d",
                    "resolvedDevicesUnique": 7412,
                    "shortCircuitAdvancedFrom": null,
                    "sourceBreakdown": {
                      "ble": {
                        "crossScreenSignals": 880,
                        "environmentalSignals": 0,
                        "repeatSignals": 5871,
                        "totalObservations": 161204,
                        "uniqueSignals": 18902
                      },
                      "gateway": {
                        "crossScreenSignals": 0,
                        "environmentalSignals": 0,
                        "repeatSignals": 0,
                        "totalObservations": 0,
                        "uniqueSignals": 0
                      },
                      "mdns": {
                        "crossScreenSignals": 62,
                        "environmentalSignals": 0,
                        "repeatSignals": 447,
                        "totalObservations": 9060,
                        "uniqueSignals": 2568
                      },
                      "wifi": {
                        "crossScreenSignals": 0,
                        "environmentalSignals": 14098,
                        "repeatSignals": 0,
                        "totalObservations": 14098,
                        "uniqueSignals": 0
                      }
                    },
                    "totalObservations": 184362,
                    "uniqueDevicesAcrossWindow": 8134,
                    "uniqueSignals": 21470,
                    "widenedFromRequested": false
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "continuityEdges": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "crossScreenSignals": {
                          "type": "integer"
                        },
                        "gateDiagnostic": {
                          "type": "object"
                        },
                        "repeatSignals": {
                          "type": "integer"
                        },
                        "sourceBreakdown": {
                          "type": "object"
                        },
                        "topPaths": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "uniqueSignals": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Proximity-graph summary"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cross-screen proximity-graph summary",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/proximity-summary\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/proximity-summary', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/proximity-summary\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/proximity-summary');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/revenue-attention-correlation": {
      "get": {
        "description": "Returns scatter-plot data showing the relationship between average\nattention and revenue per impression, point-per-creative, plus a\ntrend line. Backed by\n`audienceMetricsService.getRevenueAttentionCorrelation`.\n",
        "operationId": "getRevenueAttentionCorrelation",
        "parameters": [
          {
            "in": "query",
            "name": "timeRange",
            "schema": {
              "default": "last_7d",
              "enum": [
                "today",
                "last_7d",
                "last_30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "avgCPM": 74.88,
                    "highAttentionCPM": null,
                    "lowAttentionCPM": null,
                    "multiplier": null,
                    "totalImpressions": 24610,
                    "totalRevenue": 1842.7,
                    "trend": [
                      {
                        "date": "2026-08-30T00:00:00.000Z",
                        "impressions": 2461,
                        "revenue": 184.2
                      },
                      {
                        "date": "2026-08-31T00:00:00.000Z",
                        "impressions": 2803,
                        "revenue": 211.4
                      }
                    ]
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "points": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "trendLine": {
                          "nullable": true,
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Correlation payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Per-creative scatter of avg-attention vs revenue-per-impression",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/revenue-attention-correlation\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/revenue-attention-correlation', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/revenue-attention-correlation\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/revenue-attention-correlation');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/scene-feed": {
      "get": {
        "description": "Returns a paginated time-ordered list of scene observations across\nthe partner's screens — timestamp, scene tags, face_count, confidence,\nthumbnail_url. Backed by `sceneIntelligenceService.getSceneFeed`.\n",
        "operationId": "getSceneFeed",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 20,
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "activeDays": 21,
                    "has_more": true,
                    "nextOffset": 20,
                    "scenes": [
                      {
                        "contextual_relevance": 0.85,
                        "created_at": "2026-08-31T13:58:02.117Z",
                        "mongo_screen_id": "507f1f77bcf86cd799439011",
                        "scene_description": "A busy hotel lobby with guests checking in",
                        "screen_name": "Northwind Retail - Concourse A"
                      }
                    ],
                    "total": 118,
                    "totalObservations": 4127
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "activeDays": {
                          "description": "Distinct calendar days with observations over the last 90 days.",
                          "type": "integer"
                        },
                        "has_more": {
                          "description": "Whether another page exists after this one.",
                          "type": "boolean"
                        },
                        "nextOffset": {
                          "nullable": true,
                          "type": "integer"
                        },
                        "scenes": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "total": {
                          "description": "Distinct observations within the feed window — the exact set the feed pages over.",
                          "type": "integer"
                        },
                        "totalObservations": {
                          "description": "Distinct (screen, scene-prefix) observations over the last 90 days (gamification stat; independent of the page window).",
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Scene feed payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Paginated chronological feed of scene observations",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/scene-feed\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/scene-feed', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/scene-feed\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/scene-feed');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/analytics/scene-intelligence": {
      "get": {
        "description": "Returns the most-recent scene description for each of the partner's\nscreens — venue-class chip cloud, dominant mood, occupancy band,\nambient signals. Backed by\n`sceneIntelligenceService.getLatestSceneDescriptionsWithDiagnostic`.\nReturns at most one row per screen.\n\n`scenes: []` is ambiguous on its own — it is returned both when the\nfleet genuinely has no recent scenes and when the read could not be\nperformed (deadline, PG read failure, dependency disabled). Read\n`sceneDiagnostic.measured` to tell the two apart; do NOT infer an\noutage from an empty array, and do not treat a `measured: false`\nresponse as evidence about the fleet.\n",
        "operationId": "getSceneIntelligence",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "sceneDiagnostic": {
                      "measured": true,
                      "reason": null
                    },
                    "scenes": [
                      {
                        "contextual_relevance": 0.85,
                        "created_at": "2026-08-31T13:58:02.117Z",
                        "mongo_screen_id": "507f1f77bcf86cd799439011",
                        "scene_description": "A busy hotel lobby with guests checking in",
                        "screen_name": "Northwind Retail - Concourse A"
                      }
                    ]
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "sceneDiagnostic": {
                          "description": "Present on EVERY response, success included. A field\nthat appeared only on failure would make its absence\nthe signal, and an absent field is indistinguishable\nfrom an old client or a version skew.\n",
                          "properties": {
                            "measured": {
                              "description": "True when the scene read completed and `scenes`\nreflects the fleet. False when it did not run to\ncompletion — `scenes` is then empty for want of\nan answer, not because the fleet is quiet.\n",
                              "type": "boolean"
                            },
                            "reason": {
                              "description": "Null when `measured` is true. Otherwise names the\noperational failure.\n",
                              "enum": [
                                "timeout",
                                "error",
                                "dependency_disabled",
                                "unknown",
                                null
                              ],
                              "nullable": true,
                              "type": "string"
                            }
                          },
                          "required": [
                            "measured",
                            "reason"
                          ],
                          "type": "object"
                        },
                        "scenes": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "required": [
                        "scenes",
                        "sceneDiagnostic"
                      ],
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Scene intelligence payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Latest scene description per screen",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/analytics/scene-intelligence\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/analytics/scene-intelligence', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/analytics/scene-intelligence\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/analytics/scene-intelligence');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/api-key/rotate": {
      "post": {
        "description": "Generates a new API key with a 24-hour grace period for the old key.\nBoth keys work during the grace period, after which the old key is invalidated.\n\n**Important:** The new API key is only shown once. Store it securely!\n",
        "operationId": "rotateApiKey",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "api_key": {
                          "description": "New API key — shown only once!",
                          "example": "trb_partner_new_key_abc123",
                          "type": "string"
                        },
                        "grace_period_hours": {
                          "example": 24,
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        },
                        "previous_key_expires_at": {
                          "description": "When the old key stops working",
                          "format": "date-time",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "New API key generated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Rotate API key",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/api-key/rotate\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/api-key/rotate', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/api-key/rotate\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/api-key/rotate');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience-analytics": {
      "get": {
        "description": "Get aggregated audience analytics across all partner screens.\nProvides overview of impressions, active screens, and audience metrics.\n",
        "operationId": "getAudienceAnalytics",
        "parameters": [
          {
            "description": "Time period",
            "in": "query",
            "name": "period",
            "schema": {
              "default": "7d",
              "enum": [
                "24h",
                "7d",
                "30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "audience": {
                      "avg_attention": 0.61,
                      "avg_face_count": 2.4,
                      "screens_with_data": 9
                    },
                    "overview": {
                      "active_screens": 9,
                      "avg_impressions_per_screen": 4312,
                      "total_impressions": 51744,
                      "total_screens": 12
                    },
                    "period": "7d",
                    "timestamp": "2026-08-25T17:42:11.503Z",
                    "trends": {
                      "audience_trend": "stable",
                      "impressions_trend": "stable"
                    }
                  },
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/AudienceAnalyticsResponse"
                }
              }
            },
            "description": "Analytics retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get partner-wide audience analytics",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience-analytics\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience-analytics', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience-analytics\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience-analytics');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience-export": {
      "get": {
        "description": "Export aggregated audience data for multiple screens.\nSupports JSON and CSV formats.\n",
        "operationId": "exportAudienceData",
        "parameters": [
          {
            "description": "Comma-separated screen IDs (max 50)",
            "in": "query",
            "name": "screen_ids",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Start date (ISO 8601)",
            "in": "query",
            "name": "start_date",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "End date (ISO 8601)",
            "in": "query",
            "name": "end_date",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Export format",
            "in": "query",
            "name": "format",
            "schema": {
              "default": "json",
              "enum": [
                "json",
                "csv"
              ],
              "type": "string"
            }
          },
          {
            "description": "Data aggregation level",
            "in": "query",
            "name": "aggregation",
            "schema": {
              "default": "daily",
              "enum": [
                "hourly",
                "daily",
                "weekly"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "audience": {
                        "avgAttention": 0.61,
                        "avgDwellMs": 4820,
                        "avgFaceCount": 2.4,
                        "avg_attention": 0.61,
                        "avg_face_count": 2.4,
                        "behavior": {
                          "dominantEngagement": "medium",
                          "dominantMood": "neutral"
                        },
                        "confidence": 0.9,
                        "demographics": {},
                        "groupComposition": {
                          "couples": 0.42,
                          "families": 0.18,
                          "friends": 0.24,
                          "solo": 1.31,
                          "workGroups": 0.06
                        },
                        "incomeProfile": {
                          "dominantLevel": "medium",
                          "high": 0.28,
                          "low": 0.18,
                          "medium": 0.45,
                          "premium": 0.09
                        },
                        "income_profile": {
                          "high": 0.28,
                          "low": 0.18,
                          "medium": 0.45,
                          "premium": 0.09
                        },
                        "lifestyleSegments": [],
                        "lifestyle_segments": [],
                        "maxFaceCount": 11,
                        "period": {
                          "end": "2026-08-25T00:00:00.000Z",
                          "start": "2026-08-18T00:00:00.000Z"
                        },
                        "sampleSize": 1873
                      },
                      "country": "US",
                      "impressions": 4312,
                      "location": "Austin, TX, US",
                      "period": {
                        "end": "2026-08-25T00:00:00.000Z",
                        "start": "2026-08-18T00:00:00.000Z"
                      },
                      "screen_id": "660000000000000000000003",
                      "screen_name": "Northwind Retail - Lobby"
                    }
                  ],
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "success": true,
                  "summary": {
                    "aggregation": "daily",
                    "period": {
                      "end": "2026-08-25T00:00:00.000Z",
                      "start": "2026-08-18T00:00:00.000Z"
                    },
                    "total_impressions": 4312,
                    "total_screens": 1
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/AudienceExportResponse"
                }
              },
              "text/csv": {
                "schema": {
                  "description": "CSV formatted data",
                  "type": "string"
                }
              }
            },
            "description": "Audience data exported"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Export audience data",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience-export?screen_ids=%3Cscreen_ids%3E\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience-export?screen_ids=%3Cscreen_ids%3E', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience-export?screen_ids=%3Cscreen_ids%3E\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience-export?screen_ids=%3Cscreen_ids%3E');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/heatmap": {
      "get": {
        "description": "Returns geospatial audience heatmap data across partner screens.\nAvailable to all tiers.\n",
        "operationId": "getAudienceHeatmap",
        "parameters": [
          {
            "description": "Comma-separated screen IDs to include (omit for all screens)",
            "in": "query",
            "name": "screen_ids",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Lookback period in days",
            "in": "query",
            "name": "days",
            "schema": {
              "default": 14,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "cells": 2,
                  "heatmap": [
                    {
                      "avg_attention": 0.8,
                      "avg_face_count": 10,
                      "day_of_week": 1,
                      "hour": 9,
                      "sample_count": 20,
                      "typical_income": "high",
                      "typical_lifestyle": "Professional"
                    },
                    {
                      "avg_attention": 0.71,
                      "avg_face_count": 16.4,
                      "day_of_week": 5,
                      "hour": 18,
                      "sample_count": 143,
                      "typical_income": "medium",
                      "typical_lifestyle": "Commuter"
                    }
                  ],
                  "lookback_days": 14,
                  "object": "audience_heatmap",
                  "request_id": "req_1b8e70d4c25a49f6803ad1e5b9c72f08",
                  "screens_included": 12
                },
                "schema": {
                  "properties": {
                    "cells": {
                      "type": "integer"
                    },
                    "heatmap": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "lookback_days": {
                      "type": "integer"
                    },
                    "object": {
                      "example": "audience_heatmap",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "screens_included": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Heatmap data"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get audience heatmap data",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience/heatmap\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/heatmap', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience/heatmap\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/heatmap');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/live/{screenId}": {
      "get": {
        "description": "Returns real-time audience data including face count, attention score, mood,\ndemographics, purchase intent, and crowd density. Available to all tiers.\n",
        "operationId": "getLiveAudience",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "audience": {
                    "ad_receptivity": 0.9,
                    "attention": {
                      "attentive_face_count": 5,
                      "attentive_seconds_observed": 3.4,
                      "attentive_seconds_predicted_15s": 2.1,
                      "predicted_attention_next_30s": "medium",
                      "predicted_disengagement_imminent": false
                    },
                    "attention_score": 0.85,
                    "brand_mentions": [
                      "Northwind Electronics"
                    ],
                    "crowd_density": 12,
                    "dominant_ambience": "music",
                    "dwell_time_ms": 5000,
                    "emotional_engagement": 0.7,
                    "face_count": 8,
                    "income_level": "high",
                    "lifestyle_segment": "Professional",
                    "mood": "happy",
                    "noise_level": 45,
                    "occupancy": "10-20",
                    "purchase_intent": "medium",
                    "purchase_journey": {
                      "stage": "repeat_consideration",
                      "urgency": "medium"
                    },
                    "shopping_contexts": [
                      "gift_mission",
                      "recipient_daughter"
                    ],
                    "social": {
                      "cascade_depth": 2,
                      "social_amplification_factor": 1.4,
                      "viral_attention_score": 0.62
                    }
                  },
                  "last_updated": "2026-08-31T14:22:07.481Z",
                  "object": "audience_live",
                  "request_id": "req_9f2c4a7b1e6d80f35c8a2b41d7e09c63",
                  "screen_id": "507f1f77bcf86cd799439011",
                  "status": "live"
                },
                "schema": {
                  "properties": {
                    "audience": {
                      "description": "Null when screen is offline",
                      "nullable": true,
                      "properties": {
                        "ad_receptivity": {
                          "nullable": true,
                          "type": "number"
                        },
                        "attention_score": {
                          "type": "number"
                        },
                        "crowd_density": {
                          "type": "number"
                        },
                        "dominant_ambience": {
                          "nullable": true,
                          "type": "string"
                        },
                        "dwell_time_ms": {
                          "nullable": true,
                          "type": "number"
                        },
                        "emotional_engagement": {
                          "nullable": true,
                          "type": "number"
                        },
                        "face_count": {
                          "type": "integer"
                        },
                        "income_level": {
                          "nullable": true,
                          "type": "string"
                        },
                        "lifestyle_segment": {
                          "nullable": true,
                          "type": "string"
                        },
                        "mood": {
                          "nullable": true,
                          "type": "string"
                        },
                        "noise_level": {
                          "nullable": true,
                          "type": "number"
                        },
                        "occupancy": {
                          "nullable": true,
                          "type": "string"
                        },
                        "purchase_intent": {
                          "description": "The speech classifier's reading when the screen reported one\n(`BROWSING`, `RESEARCHING`, `COMPARING`, `READY_TO_BUY`,\n`POST_PURCHASE`, `COMPLAINING`, `NONE`), otherwise the segment the\npurchase-journey stage maps to (`high`, `medium`, `low`), otherwise null.\n",
                          "enum": [
                            "BROWSING",
                            "RESEARCHING",
                            "COMPARING",
                            "READY_TO_BUY",
                            "POST_PURCHASE",
                            "COMPLAINING",
                            "NONE",
                            "high",
                            "medium",
                            "low",
                            null
                          ],
                          "nullable": true,
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "last_updated": {
                      "format": "date-time",
                      "nullable": true,
                      "type": "string"
                    },
                    "message": {
                      "description": "Present when screen is offline",
                      "type": "string"
                    },
                    "object": {
                      "example": "audience_live",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "screen_id": {
                      "type": "string"
                    },
                    "status": {
                      "enum": [
                        "live",
                        "offline"
                      ],
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Live audience data"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Screen not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get live audience data for a screen",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience/live/<screenId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/live/<screenId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience/live/<screenId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/live/<screenId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/lookalikes/{screenId}": {
      "get": {
        "description": "Finds screens with similar audience profiles using vector similarity search.\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "getLookalikeScreens",
        "parameters": [
          {
            "description": "Source screen to find lookalikes for",
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Maximum number of matches",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 20,
              "type": "integer"
            }
          },
          {
            "description": "Filter by country",
            "in": "query",
            "name": "country",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by venue type",
            "in": "query",
            "name": "venue_type",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Minimum similarity score (0-1)",
            "in": "query",
            "name": "min_similarity",
            "schema": {
              "default": 0.7,
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "filters": {
                    "country": "US",
                    "min_similarity": 0.7,
                    "venue_type": "transit"
                  },
                  "matches": [
                    {
                      "last_seen": "2026-08-31T13:00:00.000Z",
                      "metadata": {
                        "attention": 0.83,
                        "face_count": 9.2,
                        "income": "high",
                        "lifestyle": "Professional",
                        "outcome_cpm_lift": 0.12,
                        "timestamp": "2026-08-31T13:00:00.000Z"
                      },
                      "screen_id": "65a1b2c3d4e5f6a7b8c9d0e1",
                      "similarity": 0.941
                    },
                    {
                      "last_seen": "2026-08-31T12:30:00.000Z",
                      "metadata": {
                        "attention": 0.74,
                        "face_count": 6.7,
                        "income": "medium",
                        "lifestyle": "Commuter",
                        "outcome_cpm_lift": -0.05,
                        "timestamp": "2026-08-31T12:30:00.000Z"
                      },
                      "screen_id": "65a1b2c3d4e5f6a7b8c9d0e2",
                      "similarity": 0.812
                    }
                  ],
                  "matches_count": 2,
                  "object": "audience_lookalike",
                  "request_id": "req_7c30ab415de948f2b6019dc384ea5f27",
                  "source_screen_id": "507f1f77bcf86cd799439011"
                },
                "schema": {
                  "properties": {
                    "filters": {
                      "properties": {
                        "country": {
                          "nullable": true,
                          "type": "string"
                        },
                        "min_similarity": {
                          "type": "number"
                        },
                        "venue_type": {
                          "nullable": true,
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "matches": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "matches_count": {
                      "type": "integer"
                    },
                    "object": {
                      "example": "audience_lookalike",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "source_screen_id": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Lookalike matches"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Screen not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Find lookalike screens",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience/lookalikes/<screenId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/lookalikes/<screenId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience/lookalikes/<screenId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/lookalikes/<screenId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/predict/{screenId}": {
      "get": {
        "description": "Predicts audience composition for a specific hour and day of week\nusing historical data. Requires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "predictScreenAudience",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Hour of day (UTC)",
            "in": "query",
            "name": "hour",
            "required": true,
            "schema": {
              "maximum": 23,
              "minimum": 0,
              "type": "integer"
            }
          },
          {
            "description": "Day of week (0=Sunday, 6=Saturday)",
            "in": "query",
            "name": "day",
            "required": true,
            "schema": {
              "maximum": 6,
              "minimum": 0,
              "type": "integer"
            }
          },
          {
            "description": "Historical lookback period in days",
            "in": "query",
            "name": "lookback_days",
            "schema": {
              "default": 30,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "day_of_week": 3,
                  "hour_utc": 14,
                  "lookback_days": 30,
                  "object": "audience_prediction",
                  "prediction": {
                    "confidence": 1,
                    "predicted_attention": 0.81,
                    "predicted_face_count": 7.4,
                    "sample_count": 42,
                    "typical_income": "high",
                    "typical_lifestyle": "Professional"
                  },
                  "request_id": "req_e51d7ac9026b4f38915ad0c26be3417f",
                  "screen_id": "507f1f77bcf86cd799439011"
                },
                "schema": {
                  "properties": {
                    "day_of_week": {
                      "type": "integer"
                    },
                    "hour_utc": {
                      "type": "integer"
                    },
                    "lookback_days": {
                      "type": "integer"
                    },
                    "message": {
                      "description": "Present when prediction is null",
                      "type": "string"
                    },
                    "object": {
                      "example": "audience_prediction",
                      "type": "string"
                    },
                    "prediction": {
                      "description": "Null if insufficient historical data",
                      "nullable": true,
                      "type": "object"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "screen_id": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Audience prediction"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Invalid hour or day values"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Screen not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Predict screen audience",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience/predict/<screenId>?hour=%3Chour%3E&day=%3Cday%3E\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/predict/<screenId>?hour=%3Chour%3E&day=%3Cday%3E', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience/predict/<screenId>?hour=%3Chour%3E&day=%3Cday%3E\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/predict/<screenId>?hour=%3Chour%3E&day=%3Cday%3E');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/resolved-household-reach/{campaignId}": {
      "get": {
        "description": "Returns the unique household count reached by a campaign across the\nTrillboards network, with a confidence interval, audience composition\nbreakdown, and overlap with the partner's first-party audience (when\na `pseudo_id` list was supplied at campaign creation). Underpins\ncross-channel reach reporting.\n",
        "operationId": "getResolvedHouseholdReach",
        "parameters": [
          {
            "in": "path",
            "name": "campaignId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "campaign_id": "cmp_7f3a9d21b4c6",
                    "lookback_days": 30,
                    "rows": [
                      {
                        "cross_screen_amplification_pct": 0.0912,
                        "cross_screen_devices": 388,
                        "resolved_devices_unique": 4127,
                        "screen_mongo_id": "507f1f77bcf86cd799439011"
                      },
                      {
                        "cross_screen_amplification_pct": 0.0517,
                        "cross_screen_devices": 141,
                        "resolved_devices_unique": 2610,
                        "screen_mongo_id": "65a1b2c3d4e5f6a7b8c9d0e1"
                      }
                    ],
                    "total": 2
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "audience_composition": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "campaign_id": {
                      "type": "string"
                    },
                    "confidence_interval": {
                      "properties": {
                        "lower": {
                          "type": "integer"
                        },
                        "upper": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "households_reached": {
                      "type": "integer"
                    },
                    "overlap_with_first_party": {
                      "nullable": true,
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Household reach payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Campaign not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resolved household reach for a campaign",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience/resolved-household-reach/<campaignId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/resolved-household-reach/<campaignId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience/resolved-household-reach/<campaignId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/resolved-household-reach/<campaignId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/segments": {
      "get": {
        "description": "Returns all audience segments for the partner.\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "listAudienceSegments",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "created_at": "2026-09-01T14:22:31.412Z",
                      "description": "Retail screens with sustained attention scores",
                      "id": "6a1f4c9e2b7d05f3c8ea4417",
                      "is_active": true,
                      "last_matched_at": "2026-09-01T14:24:02.108Z",
                      "last_matched_count": 3,
                      "name": "High-attention retail",
                      "object": "audience_segment",
                      "rules": [
                        {
                          "field": "venue_type",
                          "operator": "eq",
                          "value": "retail"
                        },
                        {
                          "field": "attention_score",
                          "operator": "gte",
                          "value": 0.6
                        }
                      ],
                      "updated_at": "2026-09-01T14:22:31.412Z"
                    },
                    {
                      "created_at": "2026-08-28T09:15:44.907Z",
                      "description": "",
                      "id": "6a1f4c9e2b7d05f3c8ea4418",
                      "is_active": true,
                      "last_matched_at": null,
                      "last_matched_count": 0,
                      "name": "Transit morning peak",
                      "object": "audience_segment",
                      "rules": [
                        {
                          "field": "venue_type",
                          "operator": "eq",
                          "value": "transit"
                        },
                        {
                          "field": "hour_of_day",
                          "operator": "lte",
                          "value": 10
                        }
                      ],
                      "updated_at": "2026-08-28T09:15:44.907Z"
                    }
                  ],
                  "has_more": false,
                  "object": "list",
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "total_count": 2
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/AudienceSegment"
                      },
                      "type": "array"
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "object": {
                      "example": "list",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "total_count": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Segment list"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List audience segments",
        "tags": [
          "Audience Segments"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience/segments\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/segments', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience/segments\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/segments');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "description": "Creates a custom audience segment based on rules (demographics, venue type, geography, etc.).\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "createAudienceSegment",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "name": {
                    "description": "Segment name",
                    "example": "High-Income Urban Millennials",
                    "type": "string"
                  },
                  "rules": {
                    "items": {
                      "properties": {
                        "field": {
                          "description": "Rule field (e.g., venue_type, country, city, state)",
                          "type": "string"
                        },
                        "operator": {
                          "description": "Comparison operator (e.g., eq, neq, in, gt, lt)",
                          "type": "string"
                        },
                        "value": {
                          "description": "Rule value"
                        }
                      },
                      "required": [
                        "field",
                        "operator",
                        "value"
                      ],
                      "type": "object"
                    },
                    "type": "array"
                  }
                },
                "required": [
                  "name",
                  "rules"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "created_at": "2026-09-01T14:22:31.412Z",
                  "description": "Retail screens with sustained attention scores",
                  "id": "6a1f4c9e2b7d05f3c8ea4417",
                  "is_active": true,
                  "name": "High-attention retail",
                  "object": "audience_segment",
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "rules": [
                    {
                      "field": "venue_type",
                      "operator": "eq",
                      "value": "retail"
                    },
                    {
                      "field": "attention_score",
                      "operator": "gte",
                      "value": 0.6
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/AudienceSegment"
                }
              }
            },
            "description": "Segment created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Validation error (missing name, invalid rules)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Duplicate segment name"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Segment limit exceeded"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create audience segment",
        "tags": [
          "Audience Segments"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/audience/segments\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"High-Income Urban Millennials\",\"rules\":[{\"field\":\"<field>\",\"operator\":\"<operator>\",\"value\":\"<value>\"}]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/segments', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"name\": \"High-Income Urban Millennials\",\n    \"rules\": [\n      {\n        \"field\": \"<field>\",\n        \"operator\": \"<operator>\",\n        \"value\": \"<value>\"\n      }\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/audience/segments\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"name\": \"High-Income Urban Millennials\",\n        \"rules\": [\n            {\n                \"field\": \"<field>\",\n                \"operator\": \"<operator>\",\n                \"value\": \"<value>\",\n            },\n        ],\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/segments');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'name' => 'High-Income Urban Millennials',\n        'rules' => [\n            [\n                'field' => '<field>',\n                'operator' => '<operator>',\n                'value' => '<value>',\n            ],\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/segments/{segmentId}": {
      "delete": {
        "description": "Permanently deletes an audience segment.\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "deleteAudienceSegment",
        "parameters": [
          {
            "in": "path",
            "name": "segmentId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "deleted": true,
                  "id": "6a1f4c9e2b7d05f3c8ea4417",
                  "object": "audience_segment",
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58"
                },
                "schema": {
                  "properties": {
                    "deleted": {
                      "example": true,
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "example": "audience_segment",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Segment deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Segment not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete audience segment",
        "tags": [
          "Audience Segments"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X DELETE \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.delete(\n    \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "get": {
        "description": "Returns a single audience segment by ID.\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "getAudienceSegment",
        "parameters": [
          {
            "in": "path",
            "name": "segmentId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "created_at": "2026-09-01T14:22:31.412Z",
                  "description": "Retail screens with sustained attention scores",
                  "id": "6a1f4c9e2b7d05f3c8ea4417",
                  "is_active": true,
                  "last_matched_at": "2026-09-01T14:24:02.108Z",
                  "last_matched_count": 3,
                  "name": "High-attention retail",
                  "object": "audience_segment",
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "rules": [
                    {
                      "field": "venue_type",
                      "operator": "eq",
                      "value": "retail"
                    },
                    {
                      "field": "attention_score",
                      "operator": "gte",
                      "value": 0.6
                    }
                  ],
                  "updated_at": "2026-09-01T14:22:31.412Z"
                },
                "schema": {
                  "$ref": "#/components/schemas/AudienceSegment"
                }
              }
            },
            "description": "Segment details"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Segment not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get audience segment",
        "tags": [
          "Audience Segments"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Partially updates an audience segment. All fields are optional.\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "updateAudienceSegment",
        "parameters": [
          {
            "in": "path",
            "name": "segmentId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "is_active": {
                    "type": "boolean"
                  },
                  "name": {
                    "type": "string"
                  },
                  "rules": {
                    "items": {
                      "properties": {
                        "field": {
                          "type": "string"
                        },
                        "operator": {
                          "type": "string"
                        },
                        "value": {}
                      },
                      "type": "object"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "created_at": "2026-09-01T14:22:31.412Z",
                  "description": "Retail screens with sustained attention scores",
                  "id": "6a1f4c9e2b7d05f3c8ea4417",
                  "is_active": false,
                  "last_matched_at": "2026-09-01T14:24:02.108Z",
                  "last_matched_count": 3,
                  "name": "High-attention retail",
                  "object": "audience_segment",
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "rules": [
                    {
                      "field": "venue_type",
                      "operator": "eq",
                      "value": "retail"
                    },
                    {
                      "field": "attention_score",
                      "operator": "gte",
                      "value": 0.7
                    }
                  ],
                  "updated_at": "2026-09-01T14:31:09.664Z"
                },
                "schema": {
                  "$ref": "#/components/schemas/AudienceSegment"
                }
              }
            },
            "description": "Segment updated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Validation error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Segment not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Duplicate segment name"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update audience segment",
        "tags": [
          "Audience Segments"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={},\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/segments/{segmentId}/match": {
      "post": {
        "description": "Finds screens whose audience matches the segment rules.\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "matchAudienceSegment",
        "parameters": [
          {
            "in": "path",
            "name": "segmentId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50,
              "maximum": 100,
              "type": "integer"
            }
          },
          {
            "description": "Only match screens owned by this partner",
            "in": "query",
            "name": "own_screens_only",
            "schema": {
              "default": "false",
              "enum": [
                "true",
                "false"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "matched_count": 2,
                  "matches": [
                    {
                      "city": "Brooklyn",
                      "country": "US",
                      "last_seen": "2026-09-01T14:18:02.551Z",
                      "metadata": {
                        "attention": 0.71,
                        "face_count": 3.4,
                        "income": null,
                        "lifestyle": null,
                        "outcome_cpm_lift": 0.15,
                        "timestamp": "2026-09-01T14:18:02.551Z"
                      },
                      "screen_id": "6a07660cbea5a3f52cb6b4f7",
                      "state": "NY",
                      "venue_type": "retail"
                    },
                    {
                      "city": "Jersey City",
                      "country": "US",
                      "last_seen": "2026-09-01T13:52:40.019Z",
                      "metadata": {
                        "attention": 0.63,
                        "face_count": 2.1,
                        "income": null,
                        "lifestyle": null,
                        "outcome_cpm_lift": 0,
                        "timestamp": "2026-09-01T13:52:40.019Z"
                      },
                      "screen_id": "6a0766aa4d1e07b93f2c8815",
                      "state": "NJ",
                      "venue_type": "retail"
                    }
                  ],
                  "object": "audience_segment",
                  "own_screens_only": false,
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "segment_id": "6a1f4c9e2b7d05f3c8ea4417",
                  "segment_name": "High-attention retail"
                },
                "schema": {
                  "properties": {
                    "matched_count": {
                      "type": "integer"
                    },
                    "matches": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "object": {
                      "example": "audience_segment",
                      "type": "string"
                    },
                    "own_screens_only": {
                      "type": "boolean"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "segment_id": {
                      "type": "string"
                    },
                    "segment_name": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Matching screens"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Segment not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Match screens to segment",
        "tags": [
          "Audience Segments"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/match\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/match', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/match\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/match');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/segments/{segmentId}/reach": {
      "post": {
        "description": "Estimates the potential reach of a segment including matched screens,\nprojected daily impressions, and estimated revenue.\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "estimateSegmentReach",
        "parameters": [
          {
            "in": "path",
            "name": "segmentId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "own_screens_only",
            "schema": {
              "default": "false",
              "enum": [
                "true",
                "false"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "capped_at": null,
                  "country_breakdown": {
                    "IN": 1,
                    "US": 2
                  },
                  "estimated_daily_cpm": 5.5,
                  "estimated_daily_impressions": 600,
                  "estimated_daily_revenue": 3.3,
                  "has_historical_data": true,
                  "matched_screens": 3,
                  "object": "segment_reach",
                  "online_screens": 2,
                  "segment_id": "6a1f4c9e2b7d05f3c8ea4417",
                  "segment_name": "High-attention retail",
                  "venue_breakdown": {
                    "Retail": 2,
                    "Transit": 1
                  }
                },
                "schema": {
                  "properties": {
                    "country_breakdown": {
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "type": "object"
                    },
                    "estimated_daily_cpm": {
                      "type": "number"
                    },
                    "estimated_daily_impressions": {
                      "type": "integer"
                    },
                    "estimated_daily_revenue": {
                      "type": "number"
                    },
                    "has_historical_data": {
                      "type": "boolean"
                    },
                    "matched_screens": {
                      "type": "integer"
                    },
                    "object": {
                      "example": "segment_reach",
                      "type": "string"
                    },
                    "online_screens": {
                      "type": "integer"
                    },
                    "segment_id": {
                      "type": "string"
                    },
                    "segment_name": {
                      "type": "string"
                    },
                    "venue_breakdown": {
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Reach estimation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Segment not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Estimate segment reach",
        "tags": [
          "Audience Segments"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/reach\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/reach', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/reach\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/reach');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/segments/{segmentId}/vast-tag": {
      "post": {
        "description": "Generates a VAST tag URL targeting screens that match the segment rules.\nRequires usage-based billing (`requireBilling('data_api')`).\n",
        "operationId": "generateSegmentVastTag",
        "parameters": [
          {
            "in": "path",
            "name": "segmentId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "click_through": {
                    "description": "Click-through landing page URL",
                    "format": "uri",
                    "type": "string"
                  },
                  "creative_url": {
                    "description": "Video creative URL",
                    "format": "uri",
                    "type": "string"
                  },
                  "duration": {
                    "default": 15,
                    "description": "Video duration in seconds",
                    "maximum": 120,
                    "minimum": 5,
                    "type": "integer"
                  },
                  "own_screens_only": {
                    "default": false,
                    "type": "boolean"
                  }
                },
                "required": [
                  "creative_url"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "creative": {
                    "click_through": "https://example.com/fall-sale",
                    "duration": 15,
                    "url": "https://cdn.example.com/northwind-fall-15s.mp4"
                  },
                  "cust_params": [
                    "venue_parent=retail",
                    "segment_id=6a1f4c9e2b7d05f3c8ea4417",
                    "partner_id=6a07331f0c9d42ab15e77d20"
                  ],
                  "object": "segment_vast_tag",
                  "screen_ids": [
                    "6a07660cbea5a3f52cb6b4f7",
                    "6a0766aa4d1e07b93f2c8815"
                  ],
                  "segment_id": "6a1f4c9e2b7d05f3c8ea4417",
                  "segment_name": "High-attention retail",
                  "targeted_screens": 2,
                  "vast_tag_url": "https://api.trillboards.com/v1/vast/segment?creative_url=https%3A%2F%2Fcdn.example.com%2Fnorthwind-fall-15s.mp4&duration=15&click_through=https%3A%2F%2Fexample.com%2Ffall-sale&cust_params=venue_parent%3Dretail%26segment_id%3D6a1f4c9e2b7d05f3c8ea4417%26partner_id%3D6a07331f0c9d42ab15e77d20&screen_ids=6a07660cbea5a3f52cb6b4f7,6a0766aa4d1e07b93f2c8815"
                },
                "schema": {
                  "properties": {
                    "creative": {
                      "properties": {
                        "click_through": {
                          "nullable": true,
                          "type": "string"
                        },
                        "duration": {
                          "type": "integer"
                        },
                        "url": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "cust_params": {
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    },
                    "object": {
                      "example": "segment_vast_tag",
                      "type": "string"
                    },
                    "screen_ids": {
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    },
                    "segment_id": {
                      "type": "string"
                    },
                    "segment_name": {
                      "type": "string"
                    },
                    "targeted_screens": {
                      "type": "integer"
                    },
                    "vast_tag_url": {
                      "format": "uri",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "VAST tag generated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing creative_url"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Segment not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Generate VAST tag for segment",
        "tags": [
          "Audience Segments"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/vast-tag\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"creative_url\":\"<creative_url>\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/vast-tag', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"creative_url\": \"<creative_url>\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/vast-tag\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"creative_url\": \"<creative_url>\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/segments/<segmentId>/vast-tag');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'creative_url' => '<creative_url>',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/audience/stats": {
      "get": {
        "description": "Returns high-level statistics about audience data availability for the partner's screens.\nAvailable to all tiers.\n",
        "operationId": "getAudienceStats",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data_access_tier": "developer",
                  "newest_data": "2026-08-31T14:19:55.902Z",
                  "object": "audience_stats",
                  "oldest_data": "2026-08-01T00:04:12.113Z",
                  "request_id": "req_44a1c9e0b7d2416f85309ce7a1b6d2f4",
                  "screens_with_data": 9,
                  "total_screens": 12,
                  "total_vectors": 41287
                },
                "schema": {
                  "properties": {
                    "data_access_tier": {
                      "enum": [
                        "basic",
                        "developer",
                        "enterprise"
                      ],
                      "type": "string"
                    },
                    "newest_data": {
                      "format": "date-time",
                      "nullable": true,
                      "type": "string"
                    },
                    "object": {
                      "example": "audience_stats",
                      "type": "string"
                    },
                    "oldest_data": {
                      "format": "date-time",
                      "nullable": true,
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "screens_with_data": {
                      "type": "integer"
                    },
                    "total_screens": {
                      "type": "integer"
                    },
                    "total_vectors": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Audience stats"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get audience data statistics",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/audience/stats\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/audience/stats', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/audience/stats\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/audience/stats');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/cancel": {
      "post": {
        "description": "Marks the partner's subscription for cancellation at the end of the\ncurrent billing period. The partner retains access until then; no\nproration is applied. Idempotent.\n",
        "operationId": "cancelBillingSubscription",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "cancel_at_period_end": true,
                  "cancelled": true,
                  "current_period_end": "2026-09-01T00:00:00.000Z",
                  "message": "Subscription will be cancelled at the end of the current billing period (2026-09-01T00:00:00.000Z).",
                  "object": "subscription_cancellation",
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "tier": "developer"
                },
                "schema": {
                  "properties": {
                    "effective_at": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "status": {
                      "enum": [
                        "canceled",
                        "scheduled_for_cancellation"
                      ],
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Subscription marked for cancellation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Cancel the partner's subscription at period end",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/billing/cancel\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/cancel', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/billing/cancel\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/cancel');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/credits": {
      "post": {
        "description": "Creates or retrieves the stable credit-purchase operation and confirms its Stripe payment.\nCredits are applied only after verified successful capture. If requires_action\nis true, complete Stripe confirmation with client_secret and read the committed\nbalance after settlement; HTTP201 alone does not prove funded credit.\n\nAvailable tiers:\n| Tier | Price | Credits | Bonus credit |\n|------|-------|---------|----------|\n| tier_500 | $500 | $625 | 25% |\n| tier_2000 | $2,000 | $3,100 | 55% |\n| tier_5000 | $5,000 | $10,000 | 100% |\n\nRequires an active billing setup (payment method attached).\n",
        "operationId": "purchaseCredits",
        "parameters": [
          {
            "description": "Stable caller-generated key for one purchase; reuse it on retries with the same body",
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "tier": "tier_2000"
              },
              "schema": {
                "properties": {
                  "tier": {
                    "description": "Credit purchase tier",
                    "enum": [
                      "tier_500",
                      "tier_2000",
                      "tier_5000"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "tier"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "charged_cents": 200000,
                  "credits_added_cents": "310000.00000000",
                  "discount_pct": 55,
                  "object": "credit_purchase",
                  "operation_id": "credit_purchase_example",
                  "payment_intent_id": "pi_3EXAMPLE0000000001",
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "requires_action": false,
                  "status": "succeeded"
                },
                "schema": {
                  "properties": {
                    "charged_cents": {
                      "description": "Amount charged in cents",
                      "example": 200000,
                      "type": "integer"
                    },
                    "client_secret": {
                      "description": "Present when Stripe confirmation requires action; handle as a secret",
                      "type": "string"
                    },
                    "credits_added_cents": {
                      "description": "Verified credit added in exact decimal cents; zero until capture settles",
                      "example": "310000.00000000",
                      "type": "string"
                    },
                    "discount_pct": {
                      "description": "Legacy field name: additional credit as a percentage of the charged amount",
                      "example": 55,
                      "type": "number"
                    },
                    "object": {
                      "example": "credit_purchase",
                      "type": "string"
                    },
                    "operation_id": {
                      "description": "Durable purchase operation identifier",
                      "type": "string"
                    },
                    "payment_intent_id": {
                      "description": "Stripe PaymentIntent ID",
                      "example": "pi_ABC123",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "requires_action": {
                      "description": "Complete Stripe confirmation before credit can settle",
                      "type": "boolean"
                    },
                    "status": {
                      "description": "Native Stripe PaymentIntent status",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Credits purchased"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Invalid tier or billing not set up"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Purchase committed-use credits",
        "tags": [
          "Billing"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/billing/credits\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Idempotency-Key: <Idempotency-Key>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"tier\":\"tier_2000\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/credits', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Idempotency-Key': \"<Idempotency-Key>\",\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"tier\": \"tier_2000\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/billing/credits\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n        \"Idempotency-Key\": \"<Idempotency-Key>\",\n    },\n    json={\n        \"tier\": \"tier_2000\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/credits');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Idempotency-Key: <Idempotency-Key>',\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'tier' => 'tier_2000',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "paid",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/credits/checkout": {
      "post": {
        "description": "Creates a Stripe Checkout session for purchasing committed-use credits.\nRedirect the user to the returned checkout_url to complete payment.\nCredits are automatically applied after successful payment via webhook.\n",
        "operationId": "createCreditCheckout",
        "parameters": [
          {
            "description": "Stable caller-generated key for one purchase; reuse it on retries with the same body",
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "cancel_url": "https://example.com/billing/cancel",
                "success_url": "https://example.com/billing/success",
                "tier": "tier_500"
              },
              "schema": {
                "properties": {
                  "cancel_url": {
                    "description": "URL to redirect if payment is cancelled",
                    "format": "uri",
                    "type": "string"
                  },
                  "success_url": {
                    "description": "URL to redirect after successful payment",
                    "format": "uri",
                    "type": "string"
                  },
                  "tier": {
                    "enum": [
                      "tier_500",
                      "tier_2000",
                      "tier_5000"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "tier"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_EXAMPLE0000000001",
                  "message": "Redirect the user to checkout_url to complete the purchase.",
                  "object": "checkout_session",
                  "operation_id": "credit_purchase_example",
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "session_id": "cs_test_EXAMPLE0000000001"
                },
                "schema": {
                  "properties": {
                    "checkout_url": {
                      "description": "Stripe Checkout URL — redirect user here",
                      "example": "https://checkout.stripe.com/c/pay/cs_test_abc123",
                      "format": "uri",
                      "type": "string"
                    },
                    "message": {
                      "example": "Redirect the user to checkout_url to complete the purchase.",
                      "type": "string"
                    },
                    "object": {
                      "example": "checkout_session",
                      "type": "string"
                    },
                    "operation_id": {
                      "description": "Durable credit-purchase operation identifier",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "session_id": {
                      "description": "Stripe Checkout session ID",
                      "example": "cs_test_abc123",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Checkout session created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Invalid tier"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create Stripe Checkout session for credits",
        "tags": [
          "Billing"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/billing/credits/checkout\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Idempotency-Key: <Idempotency-Key>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"tier\":\"tier_500\",\"success_url\":\"https://example.com/billing/success\",\"cancel_url\":\"https://example.com/billing/cancel\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/credits/checkout', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Idempotency-Key': \"<Idempotency-Key>\",\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"tier\": \"tier_500\",\n    \"success_url\": \"https://example.com/billing/success\",\n    \"cancel_url\": \"https://example.com/billing/cancel\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/billing/credits/checkout\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n        \"Idempotency-Key\": \"<Idempotency-Key>\",\n    },\n    json={\n        \"tier\": \"tier_500\",\n        \"success_url\": \"https://example.com/billing/success\",\n        \"cancel_url\": \"https://example.com/billing/cancel\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/credits/checkout');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Idempotency-Key: <Idempotency-Key>',\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'tier' => 'tier_500',\n        'success_url' => 'https://example.com/billing/success',\n        'cancel_url' => 'https://example.com/billing/cancel',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "paid",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/plans": {
      "get": {
        "description": "Returns the catalogue of partner billing plans — tier name, monthly\nprice in USD cents, included quotas (ads, impressions, audience-API\ncalls), and per-unit overage rates.\n",
        "operationId": "listBillingPlans",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "features": [
                        "Live audience data",
                        "Audience heatmaps",
                        "Audience stats",
                        "200 requests/minute"
                      ],
                      "name": "Basic",
                      "object": "billing_plan",
                      "price_cents": 0,
                      "rate_limit": 200,
                      "stripe_price_id": null,
                      "tier": "basic"
                    },
                    {
                      "features": [
                        "Everything in Basic",
                        "Lookalike screen search",
                        "Audience prediction",
                        "1,000 requests/minute",
                        "Priority support"
                      ],
                      "name": "Developer",
                      "object": "billing_plan",
                      "price_cents": 9900,
                      "rate_limit": 1000,
                      "stripe_price_id": "price_REDACTED_developer",
                      "tier": "developer"
                    },
                    {
                      "features": [
                        "Everything in Developer",
                        "Audience segments CRUD",
                        "Segment matching",
                        "Cross-network lookalike",
                        "5,000 requests/minute",
                        "Dedicated support",
                        "Custom integrations"
                      ],
                      "name": "Enterprise",
                      "object": "billing_plan",
                      "price_cents": 49900,
                      "rate_limit": 5000,
                      "stripe_price_id": "price_REDACTED_enterprise",
                      "tier": "enterprise"
                    }
                  ],
                  "has_more": false,
                  "object": "list",
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "total_count": 3
                },
                "schema": {
                  "properties": {
                    "plans": {
                      "items": {
                        "properties": {
                          "included_ads": {
                            "type": "integer"
                          },
                          "included_impressions": {
                            "type": "integer"
                          },
                          "overage_per_1k_impressions_cents": {
                            "type": "integer"
                          },
                          "price_cents_monthly": {
                            "type": "integer"
                          },
                          "tier": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Plan catalogue"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List available partner billing plans",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/billing/plans\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/plans', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/billing/plans\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/plans');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/setup": {
      "post": {
        "description": "Saves a Stripe payment method on your existing partner billing account.\nPaid API usage requires funded credit or an explicitly approved postpay\nallowance. Saving a payment method does not add credit or fund media.\n",
        "operationId": "setupBilling",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "payment_method_id": "pm_1OxABC123def456"
              },
              "schema": {
                "properties": {
                  "payment_method_id": {
                    "description": "Stripe payment method token obtained from Stripe.js or Stripe Elements",
                    "example": "pm_1OxABC123def456",
                    "type": "string"
                  }
                },
                "required": [
                  "payment_method_id"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "billing_status": "active",
                  "message": "Payment method saved. Fund API credits or use an explicitly approved API postpay allowance.",
                  "object": "billing_setup",
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "stripe_customer_id": "cus_EXAMPLE0000001"
                },
                "schema": {
                  "properties": {
                    "billing_status": {
                      "example": "active",
                      "type": "string"
                    },
                    "message": {
                      "example": "Payment method saved. Fund API credits or use an explicitly approved API postpay allowance.",
                      "type": "string"
                    },
                    "object": {
                      "example": "billing_setup",
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "stripe_customer_id": {
                      "example": "cus_ABC123",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Billing activated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Invalid or missing payment method"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Set up usage-based billing",
        "tags": [
          "Billing"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/billing/setup\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"payment_method_id\":\"pm_1OxABC123def456\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/setup', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"payment_method_id\": \"pm_1OxABC123def456\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/billing/setup\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"payment_method_id\": \"pm_1OxABC123def456\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/setup');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'payment_method_id' => 'pm_1OxABC123def456',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/subscribe": {
      "post": {
        "description": "Starts or upgrades a subscription to the supplied `tier`. Returns the\nStripe checkout-session URL when a card on file is missing; otherwise\nrecords the new plan and returns the active subscription.\n",
        "operationId": "createBillingSubscription",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "tier": {
                    "enum": [
                      "developer",
                      "enterprise"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "tier"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_REDACTED_EXAMPLE_SESSION",
                  "object": "checkout_session",
                  "price_cents": 9900,
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "session_id": "cs_test_REDACTED_EXAMPLE_SESSION",
                  "tier": "developer"
                },
                "schema": {
                  "properties": {
                    "checkout_url": {
                      "format": "uri",
                      "nullable": true,
                      "type": "string"
                    },
                    "status": {
                      "enum": [
                        "active",
                        "requires_payment"
                      ],
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Subscription started"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Invalid tier"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Subscribe the partner to a billing plan",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/billing/subscribe\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"tier\":\"developer\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/subscribe', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"tier\": \"developer\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/billing/subscribe\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"tier\": \"developer\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/subscribe');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'tier' => 'developer',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "paid",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/subscription": {
      "get": {
        "description": "Returns the partner's active subscription tier, billing cycle, next\ninvoice date, current-period usage roll-up, and the Stripe customer\nID. Use this to render an in-product billing-status UI.\n",
        "operationId": "getBillingSubscription",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "cancel_at_period_end": false,
                  "cancelled_at": null,
                  "current_period_end": "2026-09-01T00:00:00.000Z",
                  "current_period_start": "2026-08-01T00:00:00.000Z",
                  "is_free": false,
                  "object": "subscription_status",
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "status": "active",
                  "stripe_customer_id": "cus_REDACTED_EXAMPLE",
                  "stripe_subscription_id": "sub_REDACTED_EXAMPLE",
                  "tier": "developer",
                  "usage": {
                    "api_calls_this_period": 18422,
                    "last_reset_at": "2026-08-01T00:00:00.000Z"
                  }
                },
                "schema": {
                  "properties": {
                    "current_period_end": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "status": {
                      "enum": [
                        "active",
                        "past_due",
                        "canceled",
                        "trialing"
                      ],
                      "type": "string"
                    },
                    "stripe_customer_id": {
                      "nullable": true,
                      "type": "string"
                    },
                    "tier": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Subscription"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get the partner's current subscription",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/billing/subscription\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/subscription', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/billing/subscription\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/subscription');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/usage": {
      "get": {
        "description": "Returns counters for impressions, audience-API calls, and ads served\nin the current billing period, plus overage projection vs. plan\nquota. Refreshed every five minutes from the rollup pipeline.\n",
        "operationId": "getBillingUsage",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "api_calls_this_period": 18422,
                  "last_reset_at": "2026-08-01T00:00:00.000Z",
                  "object": "billing_usage",
                  "period_end": "2026-09-01T00:00:00.000Z",
                  "period_start": "2026-08-01T00:00:00.000Z",
                  "rate_limit": 1000,
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "tier": "developer"
                },
                "schema": {
                  "properties": {
                    "ads_served": {
                      "type": "integer"
                    },
                    "audience_api_calls": {
                      "type": "integer"
                    },
                    "impressions": {
                      "type": "integer"
                    },
                    "overage_projection_cents": {
                      "type": "integer"
                    },
                    "period_end": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "period_start": {
                      "format": "date-time",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Usage payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get the partner's current-period usage",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/billing/usage\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/usage', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/billing/usage\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/usage');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/usage-summary": {
      "get": {
        "description": "Returns committed primary-ledger balances and per-product usage for the\ncurrent UTC calendar month. Monetary cents and quantities are decimal\nstrings; preserve them as decimals rather than JavaScript floating point.\nperiod_end is exclusive. Saving a payment method or opening Checkout\ndoes not add credit: read this balance after verified payment settlement.\n",
        "operationId": "getBillingUsageSummary",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "billing_status": "active",
                  "credit_balance_cents": "5000.00000000",
                  "credit_reserved_cents": "0.00000000",
                  "object": "usage_summary",
                  "partner_mongo_id": "6a07331f0c9d42ab15e77d20",
                  "period_end": "2026-11-01T00:00:00.000Z",
                  "period_start": "2026-10-01T00:00:00.000Z",
                  "products": {
                    "data_api": {
                      "meters": {
                        "api_calls": {
                          "cost_cents": "600.00000000",
                          "free_quantity": "5000",
                          "legacy_unknown_cents": "0.00000000",
                          "paid_quantity": "3000",
                          "postpaid_cents": "0.00000000",
                          "prepaid_cents": "600.00000000",
                          "total_quantity": "8000"
                        }
                      },
                      "total_cost_cents": "600.00000000"
                    }
                  },
                  "request_id": "req_9f2c1d4b7a6e8035c1de24f0ab937c58",
                  "total_cost_cents": "600.00000000"
                },
                "schema": {
                  "properties": {
                    "billing_status": {
                      "enum": [
                        "free",
                        "none",
                        "active",
                        "suspended"
                      ],
                      "type": "string"
                    },
                    "credit_balance_cents": {
                      "description": "Committed credit balance in cents, with eight decimal places",
                      "example": "62500.00000000",
                      "type": "string"
                    },
                    "credit_reserved_cents": {
                      "description": "Credit held by in-flight reservations, in cents with eight decimal places",
                      "example": "0.00000000",
                      "type": "string"
                    },
                    "object": {
                      "example": "usage_summary",
                      "type": "string"
                    },
                    "partner_mongo_id": {
                      "description": "Partner billing account identifier",
                      "type": "string"
                    },
                    "period_end": {
                      "description": "Exclusive end of the current UTC calendar month",
                      "format": "date-time",
                      "type": "string"
                    },
                    "period_start": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "products": {
                      "additionalProperties": {
                        "properties": {
                          "meters": {
                            "additionalProperties": {
                              "properties": {
                                "cost_cents": {
                                  "type": "string"
                                },
                                "free_quantity": {
                                  "type": "string"
                                },
                                "legacy_unknown_cents": {
                                  "type": "string"
                                },
                                "paid_quantity": {
                                  "type": "string"
                                },
                                "postpaid_cents": {
                                  "type": "string"
                                },
                                "prepaid_cents": {
                                  "type": "string"
                                },
                                "total_quantity": {
                                  "description": "Total quantity in the published meter unit",
                                  "type": "string"
                                }
                              },
                              "required": [
                                "total_quantity",
                                "free_quantity",
                                "paid_quantity",
                                "cost_cents",
                                "prepaid_cents",
                                "postpaid_cents",
                                "legacy_unknown_cents"
                              ],
                              "type": "object"
                            },
                            "type": "object"
                          },
                          "total_cost_cents": {
                            "description": "Product cost in cents with eight decimal places",
                            "type": "string"
                          }
                        },
                        "required": [
                          "meters",
                          "total_cost_cents"
                        ],
                        "type": "object"
                      },
                      "type": "object"
                    },
                    "request_id": {
                      "type": "string"
                    },
                    "total_cost_cents": {
                      "description": "Period cost in cents with eight decimal places",
                      "example": "4500.00000000",
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "partner_mongo_id",
                    "billing_status",
                    "credit_balance_cents",
                    "credit_reserved_cents",
                    "period_start",
                    "period_end",
                    "products",
                    "total_cost_cents",
                    "request_id"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Usage summary"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get current billing period usage",
        "tags": [
          "Billing"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/billing/usage-summary\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/usage-summary', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/billing/usage-summary\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/usage-summary');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/billing/webhook": {
      "post": {
        "description": "Stripe POSTs subscription, invoice, and customer events to this\nendpoint. The request is verified via the `Stripe-Signature` header\nagainst the configured webhook secret; unsigned or invalid\nrequests are rejected with 400. NOT for partner use — listed here\nonly to keep the OpenAPI surface complete.\n",
        "operationId": "stripeWebhookCallback",
        "parameters": [
          {
            "in": "header",
            "name": "Stripe-Signature",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Stripe event envelope (`type`, `data`, `created`, ...). See https://stripe.com/docs/api/events/object.",
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Event processed"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing or invalid signature"
          }
        },
        "security": [],
        "summary": "Stripe webhook callback",
        "tags": [
          "Webhooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "# responds with null\ncurl -sS -X POST \"https://api.trillboards.com/v1/partner/billing/webhook\" \\\n  -H \"Stripe-Signature: <Stripe-Signature>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/billing/webhook', {\n  method: 'POST',\n  headers: {\n    'Stripe-Signature': \"<Stripe-Signature>\",\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.text(); // no response body\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/billing/webhook\",\n    headers={\n        \"Stripe-Signature\": \"<Stripe-Signature>\",\n    },\n    json={},\n)\nresponse.raise_for_status()\nbody = response.text  # no response body\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/billing/webhook');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Stripe-Signature: <Stripe-Signature>',\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n// $response is empty — do not json_decode it.\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/connect/dashboard": {
      "get": {
        "description": "Returns a comprehensive dashboard with screen counts, impression stats,\nearnings data, daily charts, and top-performing screens.\nOptimized for embedding in partner SDK dashboards.\n",
        "operationId": "getConnectDashboard",
        "parameters": [
          {
            "description": "Time period for analytics data",
            "in": "query",
            "name": "period",
            "schema": {
              "default": "30d",
              "enum": [
                "7d",
                "30d",
                "90d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "charts": {
                      "daily_impressions": [
                        {
                          "date": "2026-08-30",
                          "earnings_cents": 1531,
                          "impressions": 5104
                        },
                        {
                          "date": "2026-08-31",
                          "earnings_cents": 1463,
                          "impressions": 4877
                        }
                      ],
                      "hourly_today": [
                        {
                          "hour": 8,
                          "impressions": 214
                        }
                      ]
                    },
                    "earnings": {
                      "available_cents": 285120,
                      "daily_average_cents": 1489,
                      "lifetime_cents": 361463,
                      "pending_payout_cents": 0,
                      "period_cents": 44679,
                      "today_cents": 1446
                    },
                    "impressions": {
                      "all_time": 1204877,
                      "daily_average": 4964,
                      "period": 148930,
                      "today": 4820,
                      "trend_percent": 12
                    },
                    "partner": {
                      "cpm_cents": 300,
                      "name": "Northwind Retail Media",
                      "revenue_share_percent": 70,
                      "slug": "northwind-retail",
                      "status": "active",
                      "stripe_status": "active"
                    },
                    "period": {
                      "days": 30,
                      "end": "2026-09-01T14:22:31.000Z",
                      "start": "2026-08-02T00:00:00.000Z"
                    },
                    "screens": {
                      "offline": 3,
                      "online": 38,
                      "pending": 1,
                      "total": 42
                    },
                    "top_screens": [
                      {
                        "earnings_cents": 3852,
                        "impressions": 12840,
                        "location": "Brooklyn, NY",
                        "name": "Store 114 — Lobby",
                        "screen_id": "6a07660cbea5a3f52cb6b4f7",
                        "status": "online"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "charts": {
                          "properties": {
                            "daily_impressions": {
                              "items": {
                                "properties": {
                                  "date": {
                                    "format": "date",
                                    "type": "string"
                                  },
                                  "earnings_cents": {
                                    "type": "integer"
                                  },
                                  "impressions": {
                                    "type": "integer"
                                  }
                                },
                                "type": "object"
                              },
                              "type": "array"
                            },
                            "hourly_today": {
                              "items": {
                                "properties": {
                                  "hour": {
                                    "maximum": 23,
                                    "minimum": 0,
                                    "type": "integer"
                                  },
                                  "impressions": {
                                    "type": "integer"
                                  }
                                },
                                "type": "object"
                              },
                              "type": "array"
                            }
                          },
                          "type": "object"
                        },
                        "earnings": {
                          "properties": {
                            "available_cents": {
                              "type": "integer"
                            },
                            "daily_average_cents": {
                              "type": "integer"
                            },
                            "lifetime_cents": {
                              "type": "integer"
                            },
                            "pending_payout_cents": {
                              "type": "integer"
                            },
                            "period_cents": {
                              "type": "integer"
                            },
                            "today_cents": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "impressions": {
                          "properties": {
                            "all_time": {
                              "type": "integer"
                            },
                            "daily_average": {
                              "type": "number"
                            },
                            "period": {
                              "type": "integer"
                            },
                            "today": {
                              "type": "integer"
                            },
                            "trend_percent": {
                              "type": "number"
                            }
                          },
                          "type": "object"
                        },
                        "partner": {
                          "properties": {
                            "cpm_cents": {
                              "type": "number"
                            },
                            "name": {
                              "type": "string"
                            },
                            "revenue_share_percent": {
                              "type": "number"
                            },
                            "slug": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string"
                            },
                            "stripe_status": {
                              "enum": [
                                "pending",
                                "active",
                                "restricted",
                                "not_connected"
                              ],
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "period": {
                          "properties": {
                            "days": {
                              "type": "integer"
                            },
                            "end": {
                              "format": "date-time",
                              "type": "string"
                            },
                            "start": {
                              "format": "date-time",
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "screens": {
                          "properties": {
                            "offline": {
                              "type": "integer"
                            },
                            "online": {
                              "type": "integer"
                            },
                            "pending": {
                              "type": "integer"
                            },
                            "total": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "top_screens": {
                          "items": {
                            "properties": {
                              "earnings_cents": {
                                "type": "integer"
                              },
                              "impressions": {
                                "type": "integer"
                              },
                              "location": {
                                "nullable": true,
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "screen_id": {
                                "type": "string"
                              },
                              "status": {
                                "enum": [
                                  "online",
                                  "offline",
                                  "unknown"
                                ],
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Dashboard data"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get partner dashboard overview",
        "tags": [
          "Connect Dashboard"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/connect/dashboard\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/connect/dashboard', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/connect/dashboard\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/connect/dashboard');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/connect/earnings": {
      "get": {
        "description": "Returns daily or weekly earnings breakdown for charting.",
        "operationId": "getConnectEarningsChart",
        "parameters": [
          {
            "in": "query",
            "name": "period",
            "schema": {
              "default": "30d",
              "enum": [
                "7d",
                "30d",
                "90d"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "group_by",
            "schema": {
              "default": "day",
              "enum": [
                "day",
                "week"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "chart": [
                      {
                        "avg_view_duration_ms": 0,
                        "date": "2026-08-30T00:00:00.000Z",
                        "earnings_cents": 1531,
                        "impressions": 5104,
                        "label": "2026-08-30T00:00:00.000Z",
                        "unique_screens": 0
                      },
                      {
                        "avg_view_duration_ms": 0,
                        "date": "2026-08-31T00:00:00.000Z",
                        "earnings_cents": 1463,
                        "impressions": 4877,
                        "label": "2026-08-31T00:00:00.000Z",
                        "unique_screens": 0
                      }
                    ],
                    "period": {
                      "days": 30,
                      "end": "2026-09-01T14:22:31.000Z",
                      "start": "2026-08-02T00:00:00.000Z"
                    },
                    "summary": {
                      "data_points": 2,
                      "group_by": "day",
                      "total_earnings_cents": 2994,
                      "total_impressions": 9981
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "chart": {
                          "items": {
                            "properties": {
                              "avg_view_duration_ms": {
                                "type": "number"
                              },
                              "date": {
                                "type": "string"
                              },
                              "earnings_cents": {
                                "type": "integer"
                              },
                              "impressions": {
                                "type": "integer"
                              },
                              "label": {
                                "type": "string"
                              },
                              "unique_screens": {
                                "type": "integer"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "period": {
                          "properties": {
                            "days": {
                              "type": "integer"
                            },
                            "end": {
                              "format": "date-time",
                              "type": "string"
                            },
                            "start": {
                              "format": "date-time",
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "summary": {
                          "properties": {
                            "data_points": {
                              "type": "integer"
                            },
                            "group_by": {
                              "type": "string"
                            },
                            "total_earnings_cents": {
                              "type": "integer"
                            },
                            "total_impressions": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Earnings chart data"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get earnings chart data",
        "tags": [
          "Connect Dashboard"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/connect/earnings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/connect/earnings', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/connect/earnings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/connect/earnings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/connect/health": {
      "get": {
        "description": "Returns the online/offline status of all partner devices with heartbeat\ntimestamps, uptime percentages, and location data.\n",
        "operationId": "getConnectScreenHealth",
        "parameters": [
          {
            "description": "Filter by screen status",
            "in": "query",
            "name": "status",
            "schema": {
              "default": "all",
              "enum": [
                "online",
                "offline",
                "all"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "checked_at": "2026-09-01T14:22:31.000Z",
                    "filter": "all",
                    "screens": [
                      {
                        "device_id": "6507a1b2c3d4e5f6g7h8i9j0",
                        "external_device_id": "store-114-lobby",
                        "fingerprint": "P_a1b2c3d4e5f6g7h8",
                        "last_heartbeat": "2026-09-01T14:21:58.000Z",
                        "last_seen": "2026-09-01T14:22:04.000Z",
                        "location": {
                          "address": "540 Fulton St, Brooklyn, NY 11201",
                          "city": "Brooklyn",
                          "lat": 40.6782,
                          "lng": -73.9442,
                          "state": "NY"
                        },
                        "name": "Store 114 — Lobby",
                        "online_status": "online",
                        "registered_at": "2026-05-14T18:03:11.000Z",
                        "screen_id": "6a07660cbea5a3f52cb6b4f7",
                        "screen_status": "active",
                        "status": "online",
                        "time_since_last_seen_seconds": 27,
                        "uptime_24h_percent": 100,
                        "venue_type": "retail"
                      },
                      {
                        "device_id": "6507a1b2c3d4e5f6g7h8i9j1",
                        "external_device_id": "store-207-checkout",
                        "fingerprint": "P_b2c3d4e5f6g7h8i9",
                        "last_heartbeat": "2026-09-01T09:47:12.000Z",
                        "last_seen": "2026-09-01T09:47:12.000Z",
                        "location": {
                          "address": "30 Mall Dr W, Jersey City, NJ 07310",
                          "city": "Jersey City",
                          "lat": 40.7178,
                          "lng": -74.0431,
                          "state": "NJ"
                        },
                        "name": "Store 207 — Checkout",
                        "online_status": "offline",
                        "registered_at": "2026-06-02T11:41:56.000Z",
                        "screen_id": "6a0766aa4d1e07b93f2c8815",
                        "screen_status": "active",
                        "status": "offline",
                        "time_since_last_seen_seconds": 16519,
                        "uptime_24h_percent": 0,
                        "venue_type": "retail"
                      }
                    ],
                    "summary": {
                      "never_connected": 1,
                      "offline": 3,
                      "online": 38,
                      "total": 42
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "checked_at": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "filter": {
                          "type": "string"
                        },
                        "screens": {
                          "items": {
                            "properties": {
                              "device_id": {
                                "type": "string"
                              },
                              "external_device_id": {
                                "type": "string"
                              },
                              "fingerprint": {
                                "type": "string"
                              },
                              "last_heartbeat": {
                                "format": "date-time",
                                "nullable": true,
                                "type": "string"
                              },
                              "last_seen": {
                                "format": "date-time",
                                "nullable": true,
                                "type": "string"
                              },
                              "location": {
                                "properties": {
                                  "city": {
                                    "nullable": true,
                                    "type": "string"
                                  },
                                  "lat": {
                                    "nullable": true,
                                    "type": "number"
                                  },
                                  "lng": {
                                    "nullable": true,
                                    "type": "number"
                                  },
                                  "state": {
                                    "nullable": true,
                                    "type": "string"
                                  }
                                },
                                "type": "object"
                              },
                              "name": {
                                "type": "string"
                              },
                              "registered_at": {
                                "format": "date-time",
                                "type": "string"
                              },
                              "screen_id": {
                                "nullable": true,
                                "type": "string"
                              },
                              "screen_status": {
                                "enum": [
                                  "pending",
                                  "active",
                                  "inactive"
                                ],
                                "type": "string"
                              },
                              "status": {
                                "enum": [
                                  "online",
                                  "offline",
                                  "never_connected",
                                  "unknown"
                                ],
                                "type": "string"
                              },
                              "time_since_last_seen_seconds": {
                                "nullable": true,
                                "type": "number"
                              },
                              "uptime_24h_percent": {
                                "nullable": true,
                                "type": "number"
                              },
                              "venue_type": {
                                "nullable": true,
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "summary": {
                          "properties": {
                            "never_connected": {
                              "type": "integer"
                            },
                            "offline": {
                              "type": "integer"
                            },
                            "online": {
                              "type": "integer"
                            },
                            "total": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Screen health data"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get screen health status",
        "tags": [
          "Connect Dashboard"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/connect/health\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/connect/health', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/connect/health\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/connect/health');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/content-presets": {
      "get": {
        "description": "List all available content preference presets.\nPresets provide pre-configured content filtering rules for common venue types.\n",
        "operationId": "listContentPresets",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "blocked_advertisers": [],
                      "blocked_categories": [
                        "alcohol",
                        "gambling",
                        "adult"
                      ],
                      "description": "Safe content for family environments",
                      "id": "family_friendly",
                      "min_ad_rating": "G",
                      "name": "Family Friendly"
                    },
                    {
                      "blocked_advertisers": [],
                      "blocked_categories": [],
                      "description": "Content suitable for adult venues",
                      "id": "bar_nightclub",
                      "min_ad_rating": "PG-13",
                      "name": "Bar & Nightclub"
                    }
                  ],
                  "has_more": false,
                  "object": "list",
                  "total_count": 5,
                  "url": "/v1/partner/content-presets"
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/ContentPreset"
                      },
                      "type": "array"
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "object": {
                      "example": "list",
                      "type": "string"
                    },
                    "total_count": {
                      "type": "integer"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Content presets retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List content presets",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/content-presets\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/content-presets', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/content-presets\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/content-presets');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/content-presets/{presetId}": {
      "get": {
        "description": "Get details of a specific content preset.",
        "operationId": "getContentPreset",
        "parameters": [
          {
            "description": "Preset ID (e.g., \"family_friendly\", \"bar_nightclub\")",
            "in": "path",
            "name": "presetId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "allow_adult_content": false,
                  "allow_alcohol": false,
                  "allow_gambling": false,
                  "allow_political": false,
                  "allow_profanity": false,
                  "allow_religious": false,
                  "allow_tobacco": false,
                  "allow_violence": false,
                  "auto_approve_known_brands": true,
                  "content_filtering_enabled": true,
                  "description": "Safe for all ages. Blocks alcohol, tobacco, gambling, political, religious, adult, violent content.",
                  "id": "family-friendly",
                  "max_age_rating": "G",
                  "name": "Family-Friendly",
                  "object": "content_preset",
                  "preference_level": "strict",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083"
                },
                "schema": {
                  "$ref": "#/components/schemas/ContentPreset"
                }
              }
            },
            "description": "Preset retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Preset not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get content preset",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/content-presets/<presetId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/content-presets/<presetId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/content-presets/<presetId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/content-presets/<presetId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/creative-experiments": {
      "post": {
        "description": "Uses the existing native paid media-buy, generated creative, experiment and policy authorities. Partner and advertiser identity are derived from the authenticated credential.",
        "operationId": "createCreativeFeedbackExperiment",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackCreateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "advertisement_id": "native_ad_1",
                    "advertiser_id": "buyer_1",
                    "id": "experiment_1",
                    "media_buy_id": "media_buy_1",
                    "name": "Creative feedback",
                    "objective": "gaze_share",
                    "package_id": "owned",
                    "policy_mode": "experiment",
                    "policy_version": "1",
                    "status": "draft",
                    "variants": []
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackExperimentResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "campaigns:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "campaigns:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "bid:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "bid:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:write"
            ]
          }
        ],
        "summary": "Create an experiment on a funded native media-buy placement",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Create an experiment on a funded native media-buy placement",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}": {
      "get": {
        "description": "Read your resolved native buyer’s latest 500 generated/imported creative ledger records with explicit creatives_truncated. Same-buyer byte dedupe preserves original advertisement_mongo_id; linked_to_experiment_ad identifies current native membership. Moderation status and byte hash are present only when the global review URL exactly matches the ledger CDN URL. Brand review and global moderation remain separate. Generation/import committed retry receipts retain creativeId/renderKey so a lost response does not require another paid generation.",
        "operationId": "getCreativeFeedbackExperiment",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "advertisement_id": "native_ad_1",
                    "advertiser_id": "buyer_1",
                    "creatives": [],
                    "creatives_truncated": false,
                    "id": "experiment_1",
                    "media_buy_id": "media_buy_1",
                    "name": "Creative feedback",
                    "objective": "gaze_share",
                    "package_id": "owned",
                    "policy_mode": "experiment",
                    "policy_version": "1",
                    "status": "draft",
                    "variants": []
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackExperimentDetailResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "reports:read"
            ]
          },
          {
            "ApiKeyAuth": [
              "reports:read"
            ]
          },
          {
            "BearerAuth__sdk": [
              "stats:read"
            ]
          },
          {
            "ApiKeyAuth": [
              "stats:read"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:read"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:read"
            ]
          }
        ],
        "summary": "Read your native experiment, policy and latest 500 owned creative recovery records",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Read your native experiment, policy and latest 500 owned creative recovery records",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}/brand": {
      "post": {
        "description": "Uses the existing native paid media-buy, generated creative, experiment and policy authorities. Partner and advertiser identity are derived from the authenticated credential.",
        "operationId": "configureCreativeFeedbackBrand",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackBrandRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "advertisement_id": "native_ad_1",
                    "brand_name": "Your brand",
                    "profile_id": "bp_native",
                    "reactive_enabled": true,
                    "stance": "intent_capture"
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackBrandResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "campaigns:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "campaigns:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "bid:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "bid:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:write"
            ]
          }
        ],
        "summary": "Configure your native brand profile and enable its booked reactive campaign",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-llm-example": "Configure your native brand profile and enable its booked reactive campaign",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}/creatives/generate": {
      "post": {
        "description": "Uses the experiment’s native advertiser and advertisement. AI moderation and explicit brand approval remain separate native steps. One operation uses the existing data_api/api_calls tariff; native ad spend remains separate. An in-progress or committed idempotency key returns 409 OPERATION_IN_PROGRESS or OPERATION_ALREADY_COMMITTED; committed retry details retain error.help.usage_receipt.",
        "operationId": "generateCreativeFeedbackCreative",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackGenerateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "brandReviewStatus": "pending_brand_review",
                    "cdnUrl": "https://cdn.trillboards.com/creative.jpg",
                    "creativeId": "native_creative_1",
                    "deduped": false,
                    "headline": "A clear offer",
                    "renderKey": "render_1",
                    "status": "pending_moderation",
                    "success": true,
                    "usage_receipt": {}
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackGenerateResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "campaigns:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "campaigns:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "bid:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "bid:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:write"
            ]
          }
        ],
        "summary": "Generate a native brand creative for explicit review and moderation",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Generate a native brand creative for explicit review and moderation",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}/creatives/import": {
      "post": {
        "description": "Provide exactly one of asset_url, source_creative_id or image_base64. Inline bytes require content_type. PNG, JPEG and WebP use the native image staging and actual MIME/hash validation. source_creative_id is resolved within the authenticated partner. Brand approval and AI moderation remain explicit native steps. One operation uses the existing data_api/api_calls tariff.",
        "operationId": "importCreativeFeedbackCreative",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackImportRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "brandReviewStatus": "pending_brand_review",
                    "cdnUrl": "https://cdn.trillboards.com/creative.jpg",
                    "creativeId": "native_creative_1",
                    "deduped": false,
                    "headline": "A clear offer",
                    "renderKey": "import:actual_sha256",
                    "status": "pending_moderation",
                    "success": true,
                    "usage_receipt": {}
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackGenerateResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "campaigns:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "campaigns:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "bid:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "bid:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:write"
            ]
          }
        ],
        "summary": "Import an AI model’s image through native bytes, moderation and brand review",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Import an AI model’s image through native bytes, moderation and brand review",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}/creatives/review": {
      "post": {
        "description": "Uses the existing native paid media-buy, generated creative, experiment and policy authorities. Partner and advertiser identity are derived from the authenticated credential.",
        "operationId": "reviewCreativeFeedbackCreative",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackReviewRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "success": true
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackReviewResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "campaigns:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "campaigns:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "bid:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "bid:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:write"
            ]
          }
        ],
        "summary": "Approve or reject your generated creative using native brand review",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-llm-example": "Approve or reject your generated creative using native brand review",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}/policy": {
      "put": {
        "description": "Uses the existing native paid media-buy, generated creative, experiment and policy authorities. Partner and advertiser identity are derived from the authenticated credential.",
        "operationId": "updateCreativeFeedbackPolicy",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackPolicyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "advertisement_id": "native_ad_1",
                    "advertiser_id": "buyer_1",
                    "id": "experiment_1",
                    "media_buy_id": "media_buy_1",
                    "name": "Creative feedback",
                    "objective": "gaze_share",
                    "package_id": "owned",
                    "policy_mode": "experiment",
                    "policy_version": "1",
                    "status": "draft",
                    "variants": []
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackExperimentResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "campaigns:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "campaigns:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "bid:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "bid:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:write"
            ]
          }
        ],
        "summary": "Replace native creative weights at the expected policy version",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-llm-example": "Replace native creative weights at the expected policy version",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}/rewards": {
      "get": {
        "description": "from/to are inclusive YYYY-MM-DD dates, at most 31 days. The cursor reads latest states and tombstones; a completed scan cursor rewinds for the next poll. Deduplicate by canonical_key and reward_revision_id. build_stamp and builder_version are provenance; a physical rebuild may retain the same logical revision. It does not freeze prior database versions. Each page requires a caller-owned idempotency_key and uses one existing data_api/api_calls operation; retries with an in-progress or committed key return 409, retaining the committed receipt in error.help.usage_receipt. gaze_share maps to gaze_pct_on_screen (0–1); gaze_overlap_ms maps to its overlap in milliseconds; dwell_ms maps to dwell_capped_ms_mean. unmeasured fields remain NULL.",
        "operationId": "getCreativeFeedbackRewards",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Inclusive first play date, YYYY-MM-DD; date range is at most 31 days.",
            "in": "query",
            "name": "from",
            "required": true,
            "schema": {
              "description": "Inclusive first play date, YYYY-MM-DD; date range is at most 31 days.",
              "example": "2026-10-14",
              "format": "date",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "type": "string"
            }
          },
          {
            "description": "Inclusive last play date, YYYY-MM-DD; date range is at most 31 days.",
            "in": "query",
            "name": "to",
            "required": true,
            "schema": {
              "description": "Inclusive last play date, YYYY-MM-DD; date range is at most 31 days.",
              "example": "2026-10-14",
              "format": "date",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "type": "string"
            }
          },
          {
            "description": "Maximum native play states returned on this metered page.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "description": "Maximum native play states returned on this metered page.",
              "maximum": 5000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Opaque next_cursor from this same date scope; rewind after scan_complete.",
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "description": "Opaque next_cursor from this same date scope; rewind after scan_complete.",
              "maxLength": 8192,
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Unique caller-owned billing key for this page; committed retries return the receipt, not another read.",
            "in": "query",
            "name": "idempotency_key",
            "required": true,
            "schema": {
              "description": "Unique caller-owned billing key for this page; committed retries return the receipt, not another read.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "experiment_id": "experiment_1",
                    "next_cursor": null,
                    "objective": "gaze_share",
                    "rewind_next_poll": true,
                    "rows": [],
                    "scan_complete": true,
                    "usage_receipt": {},
                    "view": "latest_state"
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackRewardsResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "reports:read"
            ]
          },
          {
            "ApiKeyAuth": [
              "reports:read"
            ]
          },
          {
            "BearerAuth__sdk": [
              "stats:read"
            ]
          },
          {
            "ApiKeyAuth": [
              "stats:read"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:read"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:read"
            ]
          }
        ],
        "summary": "Read a metered page of latest measured native play states",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Read a metered page of latest measured native play states",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}/stop": {
      "post": {
        "description": "Uses the existing native paid media-buy, generated creative, experiment and policy authorities. Partner and advertiser identity are derived from the authenticated credential.",
        "operationId": "stopCreativeFeedbackExperiment",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackStopRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "advertisement_id": "native_ad_1",
                    "advertiser_id": "buyer_1",
                    "id": "experiment_1",
                    "media_buy_id": "media_buy_1",
                    "name": "Creative feedback",
                    "objective": "gaze_share",
                    "package_id": "owned",
                    "policy_mode": "experiment",
                    "policy_version": "1",
                    "status": "draft",
                    "variants": []
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackExperimentResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "campaigns:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "campaigns:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "bid:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "bid:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:write"
            ]
          }
        ],
        "summary": "Stop the native experiment at the expected policy version",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-llm-example": "Stop the native experiment at the expected policy version",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creative-experiments/{experiment_id}/variants": {
      "post": {
        "description": "Uses the existing native paid media-buy, generated creative, experiment and policy authorities. Partner and advertiser identity are derived from the authenticated credential.",
        "operationId": "registerCreativeFeedbackVariants",
        "parameters": [
          {
            "description": "Native experiment ID returned by create_creative_experiment.",
            "in": "path",
            "name": "experiment_id",
            "required": true,
            "schema": {
              "description": "Native experiment ID returned by create_creative_experiment.",
              "maxLength": 256,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackRegisterVariantsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "advertisement_id": "native_ad_1",
                    "advertiser_id": "buyer_1",
                    "id": "experiment_1",
                    "media_buy_id": "media_buy_1",
                    "name": "Creative feedback",
                    "objective": "gaze_share",
                    "package_id": "owned",
                    "policy_mode": "experiment",
                    "policy_version": "1",
                    "status": "draft",
                    "variants": []
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/FeedbackExperimentResponse"
                }
              }
            },
            "description": "Native operation result."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "campaigns:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "campaigns:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "bid:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "bid:write"
            ]
          },
          {
            "BearerAuth__sdk": [
              "partner:write"
            ]
          },
          {
            "ApiKeyAuth": [
              "partner:write"
            ]
          }
        ],
        "summary": "Register approved native generated creative variants",
        "tags": [
          "Creative Feedback"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-llm-example": "Register approved native generated creative variants",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/partner/creatives": {
      "post": {
        "description": "Point us at a public/CDN image or video URL and we return the same\nstructured classification the Trillboards venue portal shows for every\nserved ad: moderation verdict, age rating, confidence, IAB categories,\nsubjects, keywords, on-screen text (OCR), setting/mood/quality, and\nbrand/category.\n\nClassification is asynchronous (a few seconds for images, up to ~60s for\nvideo). The response carries a canonical `id` of the form `sha256:<hex>` —\nthe content hash we computed from the fetched bytes. Poll\n`GET /creatives/{ref}` with that id until `status` is terminal\n(`approved`, `flagged`, or `blocked`). If we have already classified the\nsame bytes, the full result is returned immediately with `200`.\n\nUse the returned `id` (or `content_sha256`) as `creative_ref` when you\npost log-level plays, so your plays join to this classification.\n",
        "operationId": "submitPartnerCreative",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "advertiser_name": {
                    "description": "Optional advertiser/brand label to attach.",
                    "example": "Trex",
                    "maxLength": 256,
                    "type": "string"
                  },
                  "media_url": {
                    "description": "Public http(s) URL of the creative image or video. Must resolve to a public address — private/internal/cloud-metadata addresses are rejected.",
                    "example": "https://cdn.example.com/ads/trex-decking.mp4",
                    "format": "uri",
                    "maxLength": 2048,
                    "type": "string"
                  }
                },
                "required": [
                  "media_url"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreativeClassification"
                }
              }
            },
            "description": "Content was already classified — full result returned immediately."
          },
          "202": {
            "content": {
              "application/json": {
                "example": {
                  "content_sha256": "6c4b8f3e2a1d…e1",
                  "id": "sha256:6c4b8f3e2a1d…e1",
                  "object": "creative_classification",
                  "status": "pending"
                },
                "schema": {
                  "$ref": "#/components/schemas/CreativeClassification"
                }
              }
            },
            "description": "Accepted for classification — poll GET /creatives/{id}."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "media_url could not be fetched as a valid image or video."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Submit a creative for classification",
        "tags": [
          "Creative Moderation"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/creatives\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"media_url\":\"https://cdn.example.com/ads/trex-decking.mp4\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/creatives', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"media_url\": \"https://cdn.example.com/ads/trex-decking.mp4\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/creatives\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"media_url\": \"https://cdn.example.com/ads/trex-decking.mp4\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/creatives');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'media_url' => 'https://cdn.example.com/ads/trex-decking.mp4',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/creatives/{ref}": {
      "get": {
        "description": "Read the classification for a creative you submitted, by its\n`sha256:<hex>` reference (or the bare 64-char hex). A partner may only\nread classifications for content it has submitted.\n",
        "operationId": "getPartnerCreative",
        "parameters": [
          {
            "description": "The reference returned by POST /creatives — `sha256:<hex>` or the bare 64-char hex.",
            "in": "path",
            "name": "ref",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "advertiser_name": "Northwind Retail",
                  "age_rating": "PG",
                  "analysis": {
                    "keywords": [
                      "decking",
                      "railing",
                      "no maintenance"
                    ],
                    "mood": "informative",
                    "quality": "professional",
                    "setting": "indoor_retail",
                    "subjects": [
                      "decking display",
                      "family on patio"
                    ],
                    "text_content": "NO MAINTENANCE DECKING"
                  },
                  "categorization": {
                    "brand": "Northwind Retail",
                    "category": "Home Improvement",
                    "iab_categories": [
                      "IAB10"
                    ]
                  },
                  "confidence": 0.93,
                  "content_sha256": "6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1",
                  "id": "sha256:6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1",
                  "moderation": {
                    "approved": true,
                    "flags": []
                  },
                  "object": "creative_classification",
                  "request_id": "req_9f2c4b8e1d7a63054c9e8f21b3d6a70e",
                  "status": "approved",
                  "thumbnail_url": null
                },
                "schema": {
                  "$ref": "#/components/schemas/CreativeClassification"
                }
              }
            },
            "description": "The creative classification (status may still be `pending`)."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get a creative classification",
        "tags": [
          "Creative Moderation"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/creatives/<ref>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/creatives/<ref>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/creatives/<ref>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/creatives/<ref>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/dashboard": {
      "get": {
        "description": "Get comprehensive dashboard overview including partner info, screen stats,\nearnings summary, top performing screens, and recent activity.\n",
        "operationId": "getDashboard",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "earnings": {
                      "available_payout_cents": 350000,
                      "next_payout_date": "2026-02-01",
                      "pending_payout_cents": 125000,
                      "revenue_share_percent": 80,
                      "total_earned_cents": 4500000
                    },
                    "partner": {
                      "created_at": "2025-12-01T00:00:00.000Z",
                      "name": "Acme Vending Co",
                      "slug": "acmevending",
                      "status": "active"
                    },
                    "recent_activity": [
                      {
                        "ad_id": "ad_123",
                        "screen_id": "507f1f77bcf86cd799439011",
                        "timestamp": "2026-01-26T12:30:00.000Z",
                        "type": "impression"
                      }
                    ],
                    "stats": {
                      "active_screens": 3420,
                      "offline_screens": 80,
                      "pending_screens": 0,
                      "total_impressions_month": 3500000,
                      "total_impressions_today": 125000,
                      "total_impressions_week": 875000,
                      "total_screens": 3500
                    },
                    "stripe_status": {
                      "account_status": "active",
                      "connect_account_id": "***"
                    },
                    "top_screens": [
                      {
                        "impressions_today": 1250,
                        "location": "Bloomington, MN",
                        "name": "Mall of America - Floor 2",
                        "screen_id": "507f1f77bcf86cd799439011"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/DashboardResponse"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Dashboard data retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get dashboard overview",
        "tags": [
          "Dashboard"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/dashboard\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/dashboard', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/dashboard\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/dashboard');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/data-export/proximity-graph-edges": {
      "get": {
        "description": "Returns the partner's cross-screen proximity-graph edges — pairs of screens, at least one of\nthem the partner's own, that the same device was seen moving between in the lookback window —\nevery edge with its counts: `edge_count` (distinct transitions), `unique_devices` and\n`lag_p50` (median minutes between the two sightings). Used by data-licensing buyers (DSP\naudience-modelling pipelines).\n\nA page at a time: the first page pins the window to end at its UTC second (`window_end`), and\nedges are ordered by `source_screen_id`, then `destination_screen_id`. Pass `next_cursor` back\nas `cursor`, with the same `days`, until `truncated` is `false`.\n",
        "operationId": "exportProximityGraphEdges",
        "parameters": [
          {
            "description": "Lookback window in days (1–90). Defaults to 7.",
            "in": "query",
            "name": "days",
            "schema": {
              "default": 7,
              "maximum": 90,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Edges per page. Defaults to 50000, the maximum; a larger value is capped at 50000.",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The previous page's `next_cursor`, passed back unchanged. Omit for the first page.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "limit": 50000,
                    "lookback_days": 7,
                    "next_cursor": null,
                    "rows": [
                      {
                        "destination_screen_id": "65a1b2c3d4e5f6a7b8c9d0e1",
                        "edge_count": 42,
                        "lag_p50": 18,
                        "source_screen_id": "507f1f77bcf86cd799439011",
                        "unique_devices": 31
                      }
                    ],
                    "total": 1,
                    "truncated": false,
                    "window_end": "2026-09-25 06:30:57"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "lookback_days": {
                          "type": "integer"
                        },
                        "next_cursor": {
                          "description": "The `cursor` of the next page, or `null` on the last page.",
                          "nullable": true,
                          "type": "string"
                        },
                        "rows": {
                          "items": {
                            "properties": {
                              "destination_screen_id": {
                                "type": "string"
                              },
                              "edge_count": {
                                "description": "Distinct transitions from the source screen to the destination screen.",
                                "type": "integer"
                              },
                              "lag_p50": {
                                "description": "Median minutes between the source and destination sightings.",
                                "type": "number"
                              },
                              "source_screen_id": {
                                "type": "string"
                              },
                              "unique_devices": {
                                "description": "Distinct devices behind those transitions.",
                                "type": "integer"
                              }
                            },
                            "required": [
                              "source_screen_id",
                              "destination_screen_id",
                              "edge_count",
                              "unique_devices",
                              "lag_p50"
                            ],
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "total": {
                          "description": "Number of edges in this page.",
                          "type": "integer"
                        },
                        "truncated": {
                          "description": "`true` when more edges follow this page: pass `next_cursor` back as `cursor`.",
                          "type": "boolean"
                        },
                        "window_end": {
                          "description": "The UTC second the window ends at (`YYYY-MM-DD HH:MM:SS`), pinned by the first page.",
                          "example": "2026-09-25 06:30:57",
                          "type": "string"
                        }
                      },
                      "required": [
                        "rows",
                        "total",
                        "lookback_days",
                        "window_end",
                        "limit",
                        "truncated",
                        "next_cursor"
                      ],
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "One page of edges with their counts, and the pinned window."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Invalid `limit` or `cursor` parameter, or a `cursor` sent with other `days` than the first page."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Export the cross-screen proximity graph edge list",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/data-export/proximity-graph-edges\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/data-export/proximity-graph-edges', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/data-export/proximity-graph-edges\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/data-export/proximity-graph-edges');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/data-export/summary": {
      "get": {
        "description": "Return per-dataset record counts and date ranges for all datasets exposed\nby `GET /data-export/{dataset}`. Restricted to screens owned by the\nauthenticated partner.\n",
        "operationId": "getDataExportSummary",
        "parameters": [
          {
            "description": "Lookback in days (max 400)",
            "in": "query",
            "name": "days",
            "schema": {
              "default": 7,
              "maximum": 400,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "datasets": {
                      "attention_ledger": {
                        "earliest": "2026-08-18T00:11:27.000Z",
                        "latest": "2026-08-25T17:30:09.000Z",
                        "total_records": 8814
                      },
                      "audience_loyalty_features": {
                        "earliest": "2026-08-18T00:01:03.000Z",
                        "latest": "2026-08-25T17:41:52.000Z",
                        "total_records": 22735
                      },
                      "audience_metrics": {
                        "earliest": "2026-08-18T00:04:12.000Z",
                        "latest": "2026-08-25T17:38:40.000Z",
                        "total_records": 41208
                      },
                      "impression_ledger": {
                        "earliest": "2026-08-18T00:02:55.000Z",
                        "latest": "2026-08-25T17:40:31.000Z",
                        "total_records": 30184
                      },
                      "intent_signals": {
                        "earliest": "2026-08-18T02:14:06.000Z",
                        "latest": "2026-08-25T16:52:18.000Z",
                        "total_records": 1140
                      },
                      "screen_metadata": {
                        "earliest": null,
                        "latest": null,
                        "total_records": 12
                      },
                      "signal_audience": {
                        "earliest": "2026-08-18T00:01:03.000Z",
                        "latest": "2026-08-25T17:41:52.000Z",
                        "total_records": 129744
                      },
                      "waterfall_events": {
                        "earliest": "2026-08-18T00:02:51.000Z",
                        "latest": "2026-08-25T17:40:29.000Z",
                        "total_records": 61392
                      }
                    },
                    "lookback_days": 7,
                    "screen_count": 12
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/DataExportSummaryResponse"
                }
              }
            },
            "description": "Per-dataset summary"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List available datasets and per-dataset record counts",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/data-export/summary\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/data-export/summary', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/data-export/summary\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/data-export/summary');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/data-export/{dataset}": {
      "get": {
        "description": "Export a partner-scoped dataset for the authenticated partner's screens, every\naggregate row with its counts. Partner screen scoping is enforced via\n`pgRepo.getEarnerScreensByPartnerMongoIds`, so a partner can never read another\npartner's rows.\n\nAvailable datasets:\n- `audience_metrics` — hourly audience aggregates (face count, attention, demographics, mood, weather)\n- `signal_audience` — hourly BLE, WiFi, and mDNS audience rollups (unique, returning,\n  and cross-screen devices, scan windows, dwell, and attention)\n- `attention_ledger` — per-impression Verified Attention Seconds (VAS) measurements\n- `impression_ledger` — settled completed impressions (vast_event / proof_of_play / partner_sdk)\n- `waterfall_events` — VAST waterfall request outcomes per ad opportunity\n- `intent_signals` — purchase / consideration / awareness signals\n- `audience_loyalty_features` — per-device cross-day loyalty features keyed by partner-scoped pseudonymous IDs\n  (`tb_` + the first 16 hex of HMAC-SHA256(the partner's salt, canonical device key)): one salt per partner,\n  whichever API key calls, so the ID is the same ref the partner's device-level files carry. Same physical\n  device produces a DIFFERENT ID for a different partner — partner-specific salt prevents cross-partner identity\n  correlation. Every identifiable radio source ships (BLE, mDNS, SSDP, HTTP probe and WiFi),\n  each row labelled in `_match_disclosure`. Master switch: the `AUDIENCE_LOYALTY_FEATURES_ENABLED`\n  runtime_config row (default true, ~5s propagation).\n- `screen_metadata` — per-screen inventory metadata: identity (`screen_id`, `unique_id`, `name`,\n  `status`, `online_status`, `last_seen`), location (`country`/`state`/`city`/`zip`,\n  `latitude`/`longitude`, `location_source` geocode provenance, `timezone`), venue classification\n  (`venue_category`/`venue_parent`/`venue_child`/`venue_subcategory`, `openooh_venue_type_id`,\n  `environment_type`, `venue_classification_source`), and projected-traffic / quality signals\n  (`projected_daily_impressions`, `places_rating`, `places_review_count`, `places_business_name`,\n  `foot_traffic_multiplier`, `commercial_value_score`, `floor_price_cpm`). This is the inventory\n  data partners hand to advertisers. No time dimension — `days` is ignored; paginate with\n  `limit`/`offset`.\n",
        "operationId": "exportDataset",
        "parameters": [
          {
            "description": "Dataset name",
            "in": "path",
            "name": "dataset",
            "required": true,
            "schema": {
              "enum": [
                "audience_metrics",
                "signal_audience",
                "attention_ledger",
                "impression_ledger",
                "waterfall_events",
                "intent_signals",
                "audience_loyalty_features",
                "screen_metadata"
              ],
              "type": "string"
            }
          },
          {
            "description": "Lookback in days (max 400)",
            "in": "query",
            "name": "days",
            "schema": {
              "default": 7,
              "maximum": 400,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Max rows to return (hard cap 50000)",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 10000,
              "maximum": 50000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Pagination offset",
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "rows": [
                      {
                        "avg_attention": 0.612,
                        "avg_dwell_ms": 4820,
                        "avg_emotional_engagement": 0.548,
                        "avg_face_count": 2.4,
                        "avg_occupancy": 6,
                        "avg_temp_f": 88.4,
                        "dominant_age_group": "26-35",
                        "dominant_gender": "female",
                        "dominant_income": "medium",
                        "dominant_mood": "neutral",
                        "hour": "2026-08-25T17:00:00.000Z",
                        "observation_count": 118,
                        "screen_mongo_id": "660000000000000000000003",
                        "weather_condition": "clear"
                      },
                      {
                        "avg_attention": 0.574,
                        "avg_dwell_ms": 4415,
                        "avg_emotional_engagement": 0.501,
                        "avg_face_count": 1.9,
                        "avg_occupancy": 5,
                        "avg_temp_f": 89.1,
                        "dominant_age_group": "36-50",
                        "dominant_gender": "male",
                        "dominant_income": "medium",
                        "dominant_mood": "neutral",
                        "hour": "2026-08-25T16:00:00.000Z",
                        "observation_count": 104,
                        "screen_mongo_id": "660000000000000000000003",
                        "weather_condition": "clear"
                      }
                    ],
                    "total": 2,
                    "truncated": false
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "lookback_days": {
                          "type": "integer"
                        },
                        "rows": {
                          "description": "Row shape depends on the requested `dataset`. For\n`audience_loyalty_features` see `AudienceLoyaltyFeaturesRow`.\nFor other datasets the row shape is documented per-dataset\nin the Trillboards data dictionary.\n",
                          "items": {
                            "oneOf": [
                              {
                                "$ref": "#/components/schemas/AudienceLoyaltyFeaturesRow"
                              },
                              {
                                "additionalProperties": true,
                                "type": "object"
                              }
                            ]
                          },
                          "type": "array"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "truncated": {
                          "type": "boolean"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Dataset exported"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Unknown dataset (response includes the list of available datasets in `available`)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Export a partner-scoped dataset",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/data-export/audience_metrics\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/data-export/audience_metrics', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/data-export/audience_metrics\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/data-export/audience_metrics');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-feature-flag": "AUDIENCE_LOYALTY_FEATURES_ENABLED",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/data/geohash-audience-panel": {
      "get": {
        "description": "Camera and radio activity per geohash-6 cell, UTC hour and registered venue category, every\nunit placed where its screen WAS: on the map-matched vehicle path at the instant, else at the\nscreen's last GPS trail fix (looking back 7 days), else, for a fixed screen, at its registered\nsite. A vehicle's registered coordinate is never used.\n\nDefinition 2 (2026-10-19). Source: `trillboards.geohash_activity_hourly`, one UTC day per build\n(the partner product `geohash_activity_hourly` v2 reads the same rows), each day at its newest\nbuild. Camera: face- and person-backed windows at their close, with faces, people in view, capped\ndwell and the head-pose index. Radio: the 6-minute slots of each screen at their midpoint, with the\nvisitor-source identifiers (BLE, mDNS, SSDP, HTTP probe) concurrent in each (summed:\n`visitor_key_slots`), split near field and far field at the pass-audience method's near-field\nboundary (`near_field_rssi_dbm`), the distinct identifiers of the cell-hour (`unique_devices`) and\ntheir pings (`observation_count`). A cell-hour no radio slot fell in (camera windows placed where no\nslot's midpoint was) carries its radio readings `null`: the radio was not read there. The approved\nmetric `radio_identifiers_per_person` (the in-cab panel's passenger near-field Bluetooth identifiers\nper person the camera read) converts identifiers of that population to people. Definition 1 filed\nevery radio ping under the screen's registry cell and carried no camera reading.\n\nEvery cell is returned with its measured counts, a page at a time: request pages with `limit` and\n`offset` until `truncated` is `false`; `next_offset` is the next page's `offset`. Rows are ordered by\n`(hour, geohash6, venue_category)`, which is unique per row. A day is rebuilt while its late camera\nwindows and trail fixes land: every page names the build it read (`built_at`), and pages that carry\ntwo builds are re-read from offset 0. A day the panel holds no build of is answered `404`\n`day_not_built`, never an empty page; an empty page is a built day whose filters matched no cell.\n\nDefault date is yesterday (UTC). Radio from 2026-05-24, camera from 2026-06-24.\n",
        "operationId": "getGeohashAudiencePanel",
        "parameters": [
          {
            "description": "Day to export (UTC). Defaults to yesterday.",
            "in": "query",
            "name": "date",
            "schema": {
              "example": "2026-10-12",
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "Optional geohash6 prefix filter (base32 alphabet `[0-9a-hjkmnp-z]`, 1–6 chars).\nMatches rows whose `geohash6` starts with the prefix.\n",
            "in": "query",
            "name": "geohash_prefix",
            "schema": {
              "example": "9qqj",
              "pattern": "^[0-9a-hjkmnp-z]{1,6}$",
              "type": "string"
            }
          },
          {
            "description": "Optional filter on the screen's registered venue category (`screens_live.venue_category`).\nAlphanumeric token, max 64 chars.\n",
            "in": "query",
            "name": "venue_category",
            "schema": {
              "example": "transit",
              "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$",
              "type": "string"
            }
          },
          {
            "description": "Rows per page. Defaults to 10000, the maximum; a larger value is capped at 10000 (a row is about 1 KB of JSON).",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 10000,
              "example": 10000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Rows to skip — the previous page's `next_offset`. Defaults to 0.",
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "example": 10000,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "built_at": "2026-10-13 01:17:42.118",
                    "date": "2026-10-12",
                    "definition_version": 2,
                    "limit": 10000,
                    "next_offset": null,
                    "offset": 0,
                    "partner_id": 42,
                    "rows": [
                      {
                        "avg_attention": 0.4351,
                        "avg_dwell_ms": 5448.8,
                        "avg_dwell_seconds": 5.449,
                        "avg_face_count": 0.9582,
                        "dwell_capped_windows": 5425,
                        "face_backed_windows": 4391,
                        "far_key_slots": 95741,
                        "far_keys_per_slot": 174.3916,
                        "fix_age_p50_s": 892,
                        "fix_age_p90_s": 2749,
                        "fixed_site_positioned_windows": 0,
                        "geohash6": "f25d9f",
                        "hour": "2026-10-12 21:00:00",
                        "income_tier": null,
                        "last_fix_positioned_windows": 329,
                        "near_field_rssi_dbm": -60,
                        "near_key_slots": 2473,
                        "near_keys_per_slot": 4.5046,
                        "observation_count": 131371,
                        "observation_windows": 5431,
                        "p95_face_count": 2,
                        "path_positioned_windows": 5102,
                        "person_backed_windows": 1040,
                        "person_reading_windows": 5431,
                        "persons_per_window": 1.037,
                        "radio_fixed_site_positioned_slots": 0,
                        "radio_last_fix_positioned_slots": 33,
                        "radio_path_positioned_slots": 516,
                        "radio_pings": 131371,
                        "radio_screens": 260,
                        "radio_slots": 549,
                        "screen_count": 260,
                        "screens": 267,
                        "unique_devices": 31028,
                        "venue_category": "transit",
                        "visitor_key_slots": 98214,
                        "visitor_keys_per_slot": 178.8962
                      }
                    ],
                    "truncated": false
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "built_at": {
                          "description": "The build of the day this page read (UTC, `YYYY-MM-DD HH:MM:SS.mmm`). A day with no build is answered `404` `day_not_built`.",
                          "example": "2026-10-13 01:17:42.118",
                          "type": "string"
                        },
                        "date": {
                          "description": "Day exported (UTC).",
                          "example": "2026-10-12",
                          "format": "date",
                          "type": "string"
                        },
                        "definition_version": {
                          "description": "The panel's definition (2 since 2026-10-19).",
                          "example": 2,
                          "type": "integer"
                        },
                        "limit": {
                          "description": "Rows per page this response was read with.",
                          "example": 10000,
                          "type": "integer"
                        },
                        "next_offset": {
                          "description": "The `offset` of the next page, or `null` on the last page.",
                          "example": null,
                          "nullable": true,
                          "type": "integer"
                        },
                        "offset": {
                          "description": "Rows skipped before this page.",
                          "example": 0,
                          "type": "integer"
                        },
                        "partner_id": {
                          "description": "Integer partner_id (`partners.partner_id`).",
                          "example": 42,
                          "type": "integer"
                        },
                        "rows": {
                          "description": "One row per (hour, geohash6, venue_category), every cell with its counts.",
                          "items": {
                            "properties": {
                              "avg_attention": {
                                "description": "Mean device head-pose index 0-1 over the camera windows. `null` when the cell-hour holds no camera window.",
                                "example": 0.4351,
                                "nullable": true,
                                "type": "number"
                              },
                              "avg_dwell_ms": {
                                "description": "Mean dwell per window in milliseconds, each window's dwell capped at its own length, over the windows that carry a start time (dwell_capped_windows). `null`: No window of the row carries a start time (dwell_capped_windows = 0).",
                                "nullable": true,
                                "type": "number"
                              },
                              "avg_dwell_seconds": {
                                "description": "Mean capped dwell per camera window, seconds (`avg_dwell_ms` / 1000). `null` when no camera window of the cell-hour carries a start time.",
                                "example": 5.449,
                                "nullable": true,
                                "type": "number"
                              },
                              "avg_face_count": {
                                "description": "Mean faces per camera window. `null` when the cell-hour holds no camera window.",
                                "example": 0.9582,
                                "nullable": true,
                                "type": "number"
                              },
                              "dwell_capped_windows": {
                                "description": "The windows avg_dwell_ms averages: camera windows that carry a start time.",
                                "type": "integer"
                              },
                              "face_backed_windows": {
                                "description": "Of those, the face-backed windows.",
                                "type": "integer"
                              },
                              "far_key_slots": {
                                "description": "Of those, the identifiers whose strongest signal was below it: the devices around the screen. Sources that report no signal strength (mDNS, SSDP, HTTP probe) are in neither split. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "nullable": true,
                                "type": "integer"
                              },
                              "far_keys_per_slot": {
                                "description": "far_key_slots / radio_slots. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "nullable": true,
                                "type": "number"
                              },
                              "fix_age_p50_s": {
                                "description": "Median age of those fixes at the window's instant. `null`: No camera window of the row was placed by a trail fix.",
                                "nullable": true,
                                "type": "integer"
                              },
                              "fix_age_p90_s": {
                                "description": "90th-percentile age of those fixes. `null`: No camera window of the row was placed by a trail fix.",
                                "nullable": true,
                                "type": "integer"
                              },
                              "fixed_site_positioned_windows": {
                                "description": "Camera windows of a fixed screen with no trail fix in the lookback, placed at its registered site.",
                                "type": "integer"
                              },
                              "geohash6": {
                                "description": "Geohash-6 cell (about 1.2 x 0.6 km) where the screen was at the unit's instant: on its map-matched path, else at its last GPS trail fix, else (a fixed screen) at its registered site.",
                                "example": "f25d9f",
                                "type": "string"
                              },
                              "hour": {
                                "description": "Start of the UTC hour, `YYYY-MM-DD HH:MM:SS`. A camera window counts in the hour it closed, a radio slot in the hour of its midpoint.",
                                "example": "2026-10-12 21:00:00",
                                "type": "string"
                              },
                              "income_tier": {
                                "description": "`null` on every row: no income source joins the panel (definition 1 wrote `unknown`).",
                                "example": null,
                                "nullable": true,
                                "type": "string"
                              },
                              "last_fix_positioned_windows": {
                                "description": "Camera windows placed at the screen's last GPS fix at or before their instant.",
                                "type": "integer"
                              },
                              "near_field_rssi_dbm": {
                                "description": "The near-field boundary of the row's day: the active pass_audience method register's rf_near_rssi_dbm, the boundary the in-cab radio uses.",
                                "type": "integer"
                              },
                              "near_key_slots": {
                                "description": "Of those, the identifiers whose strongest signal in the slot was at or above near_field_rssi_dbm: in the vehicle or against the screen. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "nullable": true,
                                "type": "integer"
                              },
                              "near_keys_per_slot": {
                                "description": "near_key_slots / radio_slots: near-field identifiers concurrent in one slot. The approved metric radio_identifiers_per_person converts near-field passenger Bluetooth identifiers to people: it is measured in the cab, where the driver's phone and the vehicle's own devices are removed by role. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "nullable": true,
                                "type": "number"
                              },
                              "observation_count": {
                                "description": "Radio observations (pings) of those identifiers in the cell-hour (`radio_pings`). `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "example": 131371,
                                "nullable": true,
                                "type": "integer"
                              },
                              "observation_windows": {
                                "description": "Camera-graded windows (face- or person-backed, ~10 s each) placed in the cell.",
                                "type": "integer"
                              },
                              "p95_face_count": {
                                "description": "Exact 95th percentile of the per-window face reading over the row's windows. `null`: No camera window in the row.",
                                "nullable": true,
                                "type": "number"
                              },
                              "path_positioned_windows": {
                                "description": "Camera windows placed on the map-matched vehicle path at their instant.",
                                "type": "integer"
                              },
                              "person_backed_windows": {
                                "description": "Of those, the person-backed windows.",
                                "type": "integer"
                              },
                              "person_reading_windows": {
                                "description": "The windows persons_per_window averages.",
                                "type": "integer"
                              },
                              "persons_per_window": {
                                "description": "Mean people in view per camera window, faced or not: per window the larger of its mean person count and its mean face count (the in-cab panel's persons reading), over the windows that carry one. `null`: No camera window of the row carries a persons reading (person_reading_windows = 0).",
                                "nullable": true,
                                "type": "number"
                              },
                              "radio_fixed_site_positioned_slots": {
                                "description": "Radio slots of a fixed screen with no trail fix in the lookback, placed at its registered site.",
                                "type": "integer"
                              },
                              "radio_last_fix_positioned_slots": {
                                "description": "Radio slots placed at the screen's last GPS fix at or before their midpoint.",
                                "type": "integer"
                              },
                              "radio_path_positioned_slots": {
                                "description": "Radio slots placed on the map-matched path at their midpoint.",
                                "type": "integer"
                              },
                              "radio_pings": {
                                "description": "Radio observations of the visitor identifiers in the row's slots. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "nullable": true,
                                "type": "integer"
                              },
                              "radio_screens": {
                                "description": "Distinct screens whose radio slots the row holds. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "nullable": true,
                                "type": "integer"
                              },
                              "radio_slots": {
                                "description": "6-minute radio slots of screens placed in the cell: a slot in which the screen's visitor radio read an identifier (a slot in which the scanner reported only the screen's own Wi-Fi access point does not show the visitor radio ran, and is no slot here). 0 when only camera windows fell in the row.",
                                "type": "integer"
                              },
                              "screen_count": {
                                "description": "Distinct screens whose radio slots the cell-hour holds (`radio_screens`). `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "example": 260,
                                "nullable": true,
                                "type": "integer"
                              },
                              "screens": {
                                "description": "Distinct screens whose camera windows the row holds.",
                                "type": "integer"
                              },
                              "unique_devices": {
                                "description": "Distinct visitor-source radio identifiers (BLE, mDNS, SSDP, HTTP probe) over the cell-hour's radio slots, exact. A distinct count: rows do not add. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "example": 31028,
                                "nullable": true,
                                "type": "integer"
                              },
                              "venue_category": {
                                "description": "The screen's registered venue category (`transit` for a vehicle screen); empty when the registry holds none.",
                                "example": "transit",
                                "type": "string"
                              },
                              "visitor_key_slots": {
                                "description": "Visitor-source radio identifiers (BLE, mDNS, SSDP, HTTP probe) distinct within each slot, summed over the slots. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "nullable": true,
                                "type": "integer"
                              },
                              "visitor_keys_per_slot": {
                                "description": "visitor_key_slots / radio_slots: identifiers concurrent in one 6-minute slot, every visitor source and both fields. `null`: No radio slot in the row (radio_slots = 0): only camera windows were placed there, and the radio was not read.",
                                "nullable": true,
                                "type": "number"
                              }
                            },
                            "required": [
                              "hour",
                              "geohash6",
                              "venue_category",
                              "income_tier",
                              "unique_devices",
                              "observation_count",
                              "screen_count",
                              "avg_dwell_seconds",
                              "avg_face_count",
                              "avg_attention",
                              "observation_windows",
                              "face_backed_windows",
                              "person_backed_windows",
                              "screens",
                              "p95_face_count",
                              "persons_per_window",
                              "person_reading_windows",
                              "avg_dwell_ms",
                              "dwell_capped_windows",
                              "path_positioned_windows",
                              "last_fix_positioned_windows",
                              "fixed_site_positioned_windows",
                              "fix_age_p50_s",
                              "fix_age_p90_s",
                              "radio_slots",
                              "radio_screens",
                              "visitor_key_slots",
                              "near_key_slots",
                              "far_key_slots",
                              "visitor_keys_per_slot",
                              "near_keys_per_slot",
                              "far_keys_per_slot",
                              "radio_pings",
                              "radio_path_positioned_slots",
                              "radio_last_fix_positioned_slots",
                              "radio_fixed_site_positioned_slots",
                              "near_field_rssi_dbm"
                            ],
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "truncated": {
                          "description": "`true` when more cells follow this page: request `next_offset`.",
                          "example": false,
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "rows",
                        "date",
                        "partner_id",
                        "limit",
                        "offset",
                        "truncated",
                        "next_offset",
                        "built_at",
                        "definition_version"
                      ],
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Every cell of the exported day with its counts, one page.\n"
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "error": "exportGeohashAudiencePanel: invalid date '2026/05/12'; expected YYYY-MM-DD",
                  "success": false
                },
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Invalid `date`, `geohash_prefix`, `venue_category`, `limit` or `offset` parameter\n(returned by the validation guards in `exportGeohashAudiencePanel`).\n"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "Partner not provisioned for data-licensing panel",
                  "success": false
                },
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Partner not provisioned for the data-licensing panel (no `partner_id` UUID)."
          },
          "404": {
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "day_not_built",
                    "doc_url": "https://api.trillboards.com/docs/errors#day_not_built",
                    "message": "No build of 2026-10-19 exists. A UTC day is built within about an hour after it closes; the panel starts on 2026-05-24.",
                    "param": "date",
                    "request_id": "req_6f1c2a9b8d7e4c3b2a1f0e9d8c7b6a5f",
                    "type": "invalid_request_error"
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/TrillboardsError"
                }
              }
            },
            "description": "`day_not_built`: the panel holds no build of the requested `date`. A UTC day is built within about an\nhour after it closes; the panel starts on 2026-05-24.\n"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Geographic audience panel (data-licensing)",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/data/geohash-audience-panel\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/data/geohash-audience-panel', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/data/geohash-audience-panel\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/data/geohash-audience-panel');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/data/outcome-attribution": {
      "get": {
        "description": "Per `(creative, screen)` \"did the ad drive an outcome\" attribution over the partner's OWN\nscreens: how many EXPOSED devices RETURNED (and crossed screens) per creative — fusing sensor\nexposure × ad play × return-visit on the canonical device identity. Sensor outcome only;\ncarries no revenue.\n\nSource-of-record: the ClickHouse `ReplacingMergeTree` spine\n`trillboards.device_outcome_exposures`, populated from `device_exposure_events` joined to\n`completed_impressions` on screen + completed-at-within-exposure-window.\n\nEvery `(creative, screen)` cell is returned with its measured counts, a page at a time.\nThe first page pins the window to end on its UTC date (`window_end`), and rows are ordered\nby `creative_id`, then `screen_mongo_id`. Pass `next_cursor` back as `cursor`, with the\nsame `days`, `grain` and `creative_id`, until `truncated` is `false`: every page reads the\nsame window and the cells after the last one returned, so no page repeats or skips a cell\nwhile the table refreshes.\n",
        "operationId": "getOutcomeAttribution",
        "parameters": [
          {
            "description": "Lookback window in days (1–90). Defaults to 7.",
            "in": "query",
            "name": "days",
            "schema": {
              "default": 7,
              "maximum": 90,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Optional single-creative filter. Omit for all creatives.",
            "in": "query",
            "name": "creative_id",
            "schema": {
              "example": "4db4bf41f2",
              "type": "string"
            }
          },
          {
            "description": "Reach grain. `person` (DEFAULT) collapses device-grain reach onto the person\nidentity crosswalk (person keys derived from face-identity templates and linked to\ndevice keys) so one human resolved across several devices is counted\nonce (`coalesce(person_id, device_key)`); low-confidence merges (< 0.5) fall back to the\ndevice key. `device` is the legacy raw per-device count. Person-grain counts are always\n≤ device-grain. See `/measurement-methodology#evidence_grade_ladder.person_resolution`.\n",
            "in": "query",
            "name": "grain",
            "schema": {
              "default": "person",
              "enum": [
                "person",
                "device"
              ],
              "type": "string"
            }
          },
          {
            "description": "Rows per page. Defaults to 50000, the maximum; a larger value is capped at 50000.",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50000,
              "example": 50000,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The previous page's `next_cursor`, passed back unchanged. Omit for the first page.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "grain": "person",
                    "limit": 50000,
                    "lookback_days": 7,
                    "next_cursor": null,
                    "rows": [
                      {
                        "avg_frequency": 1.87,
                        "creative_id": "4db4bf41f2",
                        "cross_screen_devices": 3,
                        "exposed_devices": 30,
                        "return_rate": 0.2,
                        "returned_devices": 6,
                        "screen_mongo_id": "660000000000000000000003"
                      },
                      {
                        "avg_frequency": 1.42,
                        "creative_id": "9c1ea70b53",
                        "cross_screen_devices": 1,
                        "exposed_devices": 18,
                        "return_rate": 0.1111,
                        "returned_devices": 2,
                        "screen_mongo_id": "660000000000000000000004"
                      }
                    ],
                    "total": 2,
                    "truncated": false,
                    "window_end": "2026-09-25"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "grain": {
                          "description": "The reach grain applied (echoes the `grain` query param; defaults to `person`).",
                          "enum": [
                            "person",
                            "device"
                          ],
                          "example": "person",
                          "type": "string"
                        },
                        "limit": {
                          "description": "Rows per page this response was read with.",
                          "example": 50000,
                          "type": "integer"
                        },
                        "lookback_days": {
                          "description": "The validated lookback window used.",
                          "example": 7,
                          "type": "integer"
                        },
                        "next_cursor": {
                          "description": "The `cursor` of the next page, or `null` on the last page.",
                          "example": null,
                          "nullable": true,
                          "type": "string"
                        },
                        "rows": {
                          "description": "Per-(creative, screen) outcome aggregates, every cell with its counts.",
                          "items": {
                            "properties": {
                              "avg_frequency": {
                                "description": "Average exposures per reach-identity within the (creative, screen) cell (count of detail rows / distinct reach keys). A frequency over PEOPLE under person grain, over devices under device grain.\n",
                                "example": 1.5,
                                "format": "float",
                                "type": "number"
                              },
                              "creative_id": {
                                "description": "Creative identifier from `completed_impressions.creative_id`.",
                                "example": "4db4bf41f2",
                                "type": "string"
                              },
                              "cross_screen_devices": {
                                "description": "Count of exposed identities also seen on another screen.",
                                "example": 3,
                                "type": "integer"
                              },
                              "exposed_devices": {
                                "description": "Count of distinct devices exposed to the creative on the screen.",
                                "example": 30,
                                "type": "integer"
                              },
                              "return_rate": {
                                "description": "returned_devices / exposed_devices.",
                                "example": 0.2,
                                "format": "float",
                                "type": "number"
                              },
                              "returned_devices": {
                                "description": "Count of exposed devices that returned (≤ exposed_devices).",
                                "example": 6,
                                "type": "integer"
                              },
                              "screen_mongo_id": {
                                "description": "The partner's own screen identifier.",
                                "example": "6a07660cbea5a3f52cb6b4f7",
                                "type": "string"
                              }
                            },
                            "required": [
                              "creative_id",
                              "screen_mongo_id",
                              "exposed_devices",
                              "returned_devices",
                              "return_rate",
                              "cross_screen_devices",
                              "avg_frequency"
                            ],
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "total": {
                          "description": "Number of rows in this page.",
                          "example": 1,
                          "type": "integer"
                        },
                        "truncated": {
                          "description": "`true` when more cells follow this page: pass `next_cursor` back as `cursor`.",
                          "example": false,
                          "type": "boolean"
                        },
                        "window_end": {
                          "description": "The UTC date the window ends on, pinned by the first page; the window is `window_end - lookback_days` through `window_end`.",
                          "example": "2026-09-25",
                          "format": "date",
                          "type": "string"
                        }
                      },
                      "required": [
                        "rows",
                        "total",
                        "lookback_days",
                        "grain",
                        "window_end",
                        "limit",
                        "truncated",
                        "next_cursor"
                      ],
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "One page of per-(creative, screen) outcome aggregates with their counts, plus the reach\ngrain, the pinned window and the page used.\n"
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "error": "exportOutcomeAttribution: invalid limit 'abc'; expected a positive integer",
                  "success": false
                },
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Invalid `limit` or `cursor` parameter, or a `cursor` sent with other `days`, `grain` or\n`creative_id` than the first page (returned by the page guard in\n`exportOutcomeAttribution`).\n"
          },
          "402": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Billing not set up for the `data_api` product, or the free quota is exhausted. Body carries machine-readable pricing."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Partner not provisioned for the data-licensing panel."
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Outcome-attribution export (data-licensing)",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/data/outcome-attribution\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/data/outcome-attribution', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/data/outcome-attribution\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/data/outcome-attribution');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "metered",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device": {
      "post": {
        "description": "Register a new device or update an existing one.\n\nIf a device with the same `device_id` already exists, it will be updated.\n`device_type` is the partner's mutable category. Supplying it updates the\ncategory; omitting it preserves an existing category and defaults a new\ndevice to `other`. Category updates preserve advertising classification.\n\nThe response includes an `embed_url` that should be loaded in your device's WebView.\n",
        "operationId": "registerDevice",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "custom_metadata": {
                    "description": "Any additional metadata for your records",
                    "type": "object"
                  },
                  "description": {
                    "description": "Screen or venue description for enrichment",
                    "type": "string"
                  },
                  "device_id": {
                    "description": "Your internal device identifier",
                    "example": "machine-001",
                    "type": "string"
                  },
                  "device_type": {
                    "description": "Mutable partner category; defaults to other only on a new registration. Omit it to preserve an existing device's category.",
                    "enum": [
                      "tv",
                      "tablet",
                      "display",
                      "kiosk",
                      "other",
                      "mobile",
                      "connected_tv",
                      "digital_signage",
                      "vending_machine"
                    ],
                    "type": "string"
                  },
                  "display": {
                    "description": "Screen display specification. `width`/`height` are pixel\nresolution; `width_in`/`height_in` are physical inches.\nFor accurate floor pricing we need physical inches — pass\n`width_in` + `height_in` if you've measured the screen,\nOR `ppi` so the server can derive inches from the pixel\nresolution. Pixel-only inputs (no `ppi`, no `*_in`) will\nNOT be recorded as physical size; they're still used for\norientation detection and DSP slot dimensions.\n",
                    "properties": {
                      "height": {
                        "description": "Display resolution height in pixels (e.g. 1080)",
                        "example": 1080,
                        "type": "integer"
                      },
                      "height_in": {
                        "description": "Physical screen height in INCHES. Pairs with `width_in`.",
                        "example": 26.8,
                        "type": "number"
                      },
                      "orientation": {
                        "default": "landscape",
                        "enum": [
                          "landscape",
                          "portrait"
                        ],
                        "type": "string"
                      },
                      "ppi": {
                        "description": "Pixels per inch. Required if you want physical screen\nsize derived from `width`/`height` (server computes\nwidthInches = width / ppi). Typical: 40 (large TV),\n96 (desktop monitor), 264 (tablet).\n",
                        "example": 40,
                        "type": "number"
                      },
                      "width": {
                        "description": "Display resolution width in pixels (e.g. 1920)",
                        "example": 1920,
                        "type": "integer"
                      },
                      "width_in": {
                        "description": "Physical screen width in INCHES. Overrides ppi-based\nderivation. Use this when you've measured the actual\nscreen (most accurate for pricing).\n",
                        "example": 47.6,
                        "type": "number"
                      }
                    },
                    "type": "object"
                  },
                  "display_settings": {
                    "$ref": "#/components/schemas/PartnerRegistrationDisplaySettings"
                  },
                  "location": {
                    "properties": {
                      "address": {
                        "example": "60 E Broadway, Bloomington, MN",
                        "type": "string"
                      },
                      "city": {
                        "example": "Bloomington",
                        "type": "string"
                      },
                      "country": {
                        "example": "US",
                        "type": "string"
                      },
                      "lat": {
                        "example": 44.8537,
                        "format": "double",
                        "type": "number"
                      },
                      "lng": {
                        "example": -93.2428,
                        "format": "double",
                        "type": "number"
                      },
                      "state": {
                        "example": "MN",
                        "type": "string"
                      },
                      "timezone": {
                        "example": "America/Chicago",
                        "type": "string"
                      },
                      "venue_name": {
                        "example": "Mall of America",
                        "type": "string"
                      },
                      "venue_type": {
                        "enum": [
                          "mall",
                          "airport",
                          "retail",
                          "restaurant",
                          "office",
                          "gym",
                          "hospital",
                          "school",
                          "other"
                        ],
                        "type": "string"
                      },
                      "zip": {
                        "example": 55425,
                        "type": "string"
                      }
                    },
                    "type": "object"
                  },
                  "mobile_settings": {
                    "description": "Reporting hints for moving screens (mobility_type mobile/semi_mobile).",
                    "properties": {
                      "distance_threshold_meters": {
                        "default": 100,
                        "description": "Minimum displacement before a fix is sent (device-side hint).",
                        "type": "integer"
                      },
                      "update_interval_seconds": {
                        "default": 30,
                        "description": "Target GPS reporting interval (floor — the platform adapts to speed/battery).",
                        "type": "integer"
                      },
                      "vehicle_type": {
                        "enum": [
                          "taxi",
                          "rideshare",
                          "delivery",
                          "bus",
                          "food_truck",
                          "other"
                        ],
                        "type": "string"
                      }
                    },
                    "type": "object"
                  },
                  "mobility_type": {
                    "default": "static",
                    "description": "Declare a moving screen. `mobile` (taxi-top, transit) and\n`semi_mobile` (food truck, pop-up) screens report GPS via the\nheartbeat `location` field; `static` (default) screens ignore it.\n",
                    "enum": [
                      "static",
                      "mobile",
                      "semi_mobile"
                    ],
                    "type": "string"
                  },
                  "name": {
                    "description": "Friendly name for the device",
                    "example": "Mall of America - Floor 2",
                    "type": "string"
                  },
                  "photo_url": {
                    "description": "Photo URL for enrichment context",
                    "format": "uri",
                    "type": "string"
                  },
                  "photos": {
                    "description": "Optional additional photos for enrichment",
                    "items": {
                      "properties": {
                        "type": {
                          "description": "photo type (context, close, angle, other)",
                          "type": "string"
                        },
                        "url": {
                          "format": "uri",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "type": "array"
                  },
                  "placement": {
                    "description": "Physical placement details (optional)",
                    "properties": {
                      "height_from_floor_ft": {
                        "example": 5,
                        "type": "number"
                      },
                      "mounting_type": {
                        "enum": [
                          "wall",
                          "ceiling",
                          "tabletop",
                          "window",
                          "stand",
                          "freestanding",
                          "floor_stand",
                          "counter",
                          "pole",
                          "vehicle",
                          "other",
                          "unknown"
                        ],
                        "example": "wall",
                        "type": "string"
                      },
                      "orientation": {
                        "enum": [
                          "landscape",
                          "portrait"
                        ],
                        "example": "landscape",
                        "type": "string"
                      },
                      "viewing_distance_ft": {
                        "example": 8,
                        "type": "number"
                      }
                    },
                    "type": "object"
                  },
                  "venue": {
                    "description": "Venue classification details (optional). Subcategory is free text and may be auto-enriched by AI.",
                    "properties": {
                      "business_name": {
                        "type": "string"
                      },
                      "category": {
                        "enum": [
                          "retail",
                          "restaurant",
                          "bar",
                          "cafe",
                          "hotel",
                          "office",
                          "gym",
                          "hospital",
                          "school",
                          "mall",
                          "airport",
                          "gas_station",
                          "convenience_store",
                          "laundromat",
                          "salon",
                          "auto_shop",
                          "other"
                        ],
                        "example": "retail",
                        "type": "string"
                      },
                      "context_notes": {
                        "type": "string"
                      },
                      "environment_type": {
                        "enum": [
                          "indoor",
                          "outdoor",
                          "semi_outdoor",
                          "transit"
                        ],
                        "example": "indoor",
                        "type": "string"
                      },
                      "placement_type": {
                        "enum": [
                          "digital_signage",
                          "point_of_sale",
                          "waiting_area",
                          "window_display",
                          "kiosk",
                          "other"
                        ],
                        "example": "digital_signage",
                        "type": "string"
                      },
                      "subcategory": {
                        "description": "Free text subcategory (may be auto-enriched by AI)",
                        "type": "string"
                      }
                    },
                    "type": "object"
                  }
                },
                "required": [
                  "device_id"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "device_id": "6507a1b2c3d4e5f6g7h8i9j0",
                    "device_type": "other",
                    "embed_url": "https://screen.trillboards.com?fp=P_a1b2c3d4e5f6g7h8",
                    "enrichment_status": "pending",
                    "external_device_id": "machine-001",
                    "fingerprint": "P_a1b2c3d4e5f6g7h8",
                    "screen_id": "6507a1b2c3d4e5f6g7h8aa11",
                    "status": "active"
                  },
                  "message": "Device registered",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "device_id": {
                          "description": "Our internal device ID",
                          "type": "string"
                        },
                        "device_type": {
                          "description": "Saved partner category, independent of advertising classification.",
                          "enum": [
                            "tv",
                            "tablet",
                            "display",
                            "kiosk",
                            "other",
                            "mobile",
                            "connected_tv",
                            "digital_signage",
                            "vending_machine"
                          ],
                          "type": "string"
                        },
                        "embed_url": {
                          "description": "Load this URL in your WebView",
                          "type": "string"
                        },
                        "enrichment_status": {
                          "description": "Enrichment completion state",
                          "type": "string"
                        },
                        "external_device_id": {
                          "description": "Your device ID",
                          "type": "string"
                        },
                        "fingerprint": {
                          "description": "Device fingerprint for SDK",
                          "type": "string"
                        },
                        "location_capability": {
                          "$ref": "#/components/schemas/PartnerLocationCapability"
                        },
                        "monetization_status": {
                          "description": "Stripe-style readiness. `ready` — the screen has its own\ncoordinates and its venue is classified. `limited` —\nservable, but not on every rail: the venue is not yet\nclassified, or the screen has no position of its own and is\nlisted in inventory feeds at your fleet's market location.\n`blocked` — no rail has a location, or classification\npermanently failed. A device is never refused registration\nfor missing location.\n",
                          "enum": [
                            "ready",
                            "limited",
                            "blocked"
                          ],
                          "type": "string"
                        },
                        "requirements": {
                          "$ref": "#/components/schemas/PartnerMonetizationRequirements"
                        },
                        "screen_id": {
                          "description": "Linked screen ID",
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "example": "Device registered",
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Device registered",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Register a device",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/device\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device_id\":\"machine-001\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"device_id\": \"machine-001\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/device\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"device_id\": \"machine-001\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'device_id' => 'machine-001',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}": {
      "delete": {
        "description": "Soft-delete a device. It will no longer receive ads.",
        "operationId": "deleteDevice",
        "parameters": [
          {
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "message": "Device deleted",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "message": {
                      "example": "Device deleted",
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Device deleted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete a device",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X DELETE \"https://api.trillboards.com/v1/partner/device/<deviceId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.delete(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "get": {
        "operationId": "getDevice",
        "parameters": [
          {
            "description": "Device ID, fingerprint, or external device ID",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "created_at": "2026-07-14T18:03:22.401Z",
                    "device_id": "6624f1a0c3d4e5f6a7b8c9d0",
                    "device_type": "digital_signage",
                    "display": {
                      "height": 1080,
                      "orientation": "landscape",
                      "ppi": 96,
                      "width": 1920
                    },
                    "embed_url": "https://screen.trillboards.com?fp=P_a1b2c3d4e5f6a7b8c9d0e1f2",
                    "enrichment": {
                      "confidence": 0.92,
                      "status": "complete",
                      "venue_category": "retail"
                    },
                    "external_device_id": "lobby-nyc-001",
                    "fingerprint": "P_a1b2c3d4e5f6a7b8c9d0e1f2",
                    "location": {
                      "address": "1 Example Plaza, New York, NY 10004, USA",
                      "city": "New York",
                      "country": "US",
                      "lat": 40.7128,
                      "lng": -74.006,
                      "state": "NY",
                      "timezone": "America/New_York",
                      "venue_name": "Northwind Retail Broadway Lobby",
                      "venue_type": "malls",
                      "zip": "10004"
                    },
                    "location_capability": {
                      "position": "precise",
                      "rails": {
                        "geo_targeted_demand": {
                          "location_source": "screen",
                          "reason": null,
                          "status": "ready"
                        },
                        "inventory_feed": {
                          "location_source": "screen",
                          "reason": null,
                          "status": "ready"
                        },
                        "visit_attribution": {
                          "location_source": "screen",
                          "reason": null,
                          "status": "ready"
                        }
                      }
                    },
                    "monetization_status": "ready",
                    "name": "Northwind Retail Broadway Lobby",
                    "requirements": {
                      "currently_due": [],
                      "eventually_due": []
                    },
                    "screen_id": "6624f1a0c3d4e5f6a7b8ca11",
                    "status": "approved",
                    "updated_at": "2026-08-28T09:12:47.883Z",
                    "venue": {
                      "category": "retail",
                      "environment_type": "indoor",
                      "openooh_venue_type_id": 205,
                      "placement_type": "digital_signage",
                      "subcategory": "malls"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Device"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Device info"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get device info",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/device/<deviceId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Partially update device metadata. Only provided fields are updated.\n`device_type` changes only the partner category. Advertising type,\nhardware identity, and bidding settings remain unchanged. Unsupported\ncategories return HTTP 400 with `INVALID_DEVICE_TYPE`.\nChanges propagate to the linked screen record.\nIf location changes, venue enrichment is re-triggered.\n",
        "operationId": "updateDevice",
        "parameters": [
          {
            "description": "Device ID, fingerprint, or external device ID",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "custom_metadata": {
                    "description": "Any additional metadata for your records",
                    "type": "object"
                  },
                  "description": {
                    "description": "Screen or venue description",
                    "type": "string"
                  },
                  "device_type": {
                    "description": "Mutable partner category. Omit it to preserve the current value; changing it does not alter advertising classification.",
                    "enum": [
                      "tv",
                      "tablet",
                      "display",
                      "kiosk",
                      "other",
                      "mobile",
                      "connected_tv",
                      "digital_signage",
                      "vending_machine"
                    ],
                    "type": "string"
                  },
                  "display": {
                    "description": "Screen display specification. `width`/`height` are pixels;\npass `width_in`/`height_in` (inches) or `ppi` to record\nphysical screen size for floor pricing. See POST /device\nfor full unit semantics.\n",
                    "properties": {
                      "height": {
                        "description": "Display resolution height in pixels",
                        "type": "integer"
                      },
                      "height_in": {
                        "description": "Physical screen height in inches (pairs with width_in)",
                        "type": "number"
                      },
                      "orientation": {
                        "enum": [
                          "landscape",
                          "portrait"
                        ],
                        "type": "string"
                      },
                      "ppi": {
                        "description": "Pixels per inch (required to derive physical size from width/height)",
                        "type": "number"
                      },
                      "width": {
                        "description": "Display resolution width in pixels",
                        "type": "integer"
                      },
                      "width_in": {
                        "description": "Physical screen width in inches (overrides ppi-based derivation)",
                        "type": "number"
                      }
                    },
                    "type": "object"
                  },
                  "external_device_id": {
                    "description": "Partner-owned device ID used for idempotent lookup and reconciliation",
                    "type": "string"
                  },
                  "location": {
                    "properties": {
                      "address": {
                        "type": "string"
                      },
                      "city": {
                        "type": "string"
                      },
                      "country": {
                        "type": "string"
                      },
                      "lat": {
                        "format": "double",
                        "type": "number"
                      },
                      "lng": {
                        "format": "double",
                        "type": "number"
                      },
                      "state": {
                        "type": "string"
                      },
                      "timezone": {
                        "type": "string"
                      },
                      "venue_type": {
                        "description": "Alias for `venue.category` — a granular venue type. Accepted on PATCH (previously documented on register only). When both are sent, `venue.category` wins. On the response this field returns the OpenOOH CHILD slug (e.g. \"hotels\"/\"malls\"); `venue.category` returns the parent.\n",
                        "enum": [
                          "mall",
                          "airport",
                          "retail",
                          "restaurant",
                          "office",
                          "gym",
                          "hospital",
                          "school",
                          "other"
                        ],
                        "type": "string"
                      },
                      "zip": {
                        "type": "string"
                      }
                    },
                    "type": "object"
                  },
                  "mobile_settings": {
                    "description": "Reporting hints for moving screens. Merges with existing settings.",
                    "properties": {
                      "distance_threshold_meters": {
                        "type": "integer"
                      },
                      "update_interval_seconds": {
                        "type": "integer"
                      },
                      "vehicle_type": {
                        "enum": [
                          "taxi",
                          "rideshare",
                          "delivery",
                          "bus",
                          "food_truck",
                          "other"
                        ],
                        "type": "string"
                      }
                    },
                    "type": "object"
                  },
                  "mobility_type": {
                    "description": "Update the screen's mobility. Set `mobile`/`semi_mobile` to have\nthe heartbeat `location` field accepted for a moving screen.\n",
                    "enum": [
                      "static",
                      "mobile",
                      "semi_mobile"
                    ],
                    "type": "string"
                  },
                  "name": {
                    "description": "Friendly name for the device",
                    "type": "string"
                  },
                  "photo_url": {
                    "format": "uri",
                    "type": "string"
                  },
                  "photos": {
                    "items": {
                      "properties": {
                        "type": {
                          "type": "string"
                        },
                        "url": {
                          "format": "uri",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "type": "array"
                  },
                  "placement": {
                    "properties": {
                      "height_from_floor_ft": {
                        "type": "number"
                      },
                      "mounting_type": {
                        "enum": [
                          "wall",
                          "ceiling",
                          "tabletop",
                          "window",
                          "stand",
                          "freestanding",
                          "floor_stand",
                          "counter",
                          "pole",
                          "vehicle",
                          "other",
                          "unknown"
                        ],
                        "type": "string"
                      },
                      "orientation": {
                        "enum": [
                          "landscape",
                          "portrait"
                        ],
                        "type": "string"
                      },
                      "viewing_distance_ft": {
                        "type": "number"
                      }
                    },
                    "type": "object"
                  },
                  "venue": {
                    "properties": {
                      "business_name": {
                        "type": "string"
                      },
                      "category": {
                        "description": "Granular venue type on WRITE (child-level slug). On READ this returns the resolved OpenOOH PARENT (e.g. \"hotel\" write -> \"entertainment\" read); the child slug rides `venue.subcategory` and the numeric child id rides `venue.openooh_venue_type_id`.\n",
                        "enum": [
                          "retail",
                          "restaurant",
                          "bar",
                          "cafe",
                          "hotel",
                          "office",
                          "gym",
                          "hospital",
                          "school",
                          "mall",
                          "airport",
                          "gas_station",
                          "convenience_store",
                          "laundromat",
                          "salon",
                          "auto_shop",
                          "other"
                        ],
                        "type": "string"
                      },
                      "context_notes": {
                        "type": "string"
                      },
                      "environment_type": {
                        "enum": [
                          "indoor",
                          "outdoor",
                          "semi_outdoor",
                          "transit"
                        ],
                        "type": "string"
                      },
                      "openooh_venue_type_id": {
                        "description": "Response-only. Canonical OpenOOH child id to reconcile the declared venue type against (e.g. 807 for hotels).",
                        "type": "integer"
                      },
                      "placement_type": {
                        "enum": [
                          "digital_signage",
                          "point_of_sale",
                          "waiting_area",
                          "window_display",
                          "kiosk",
                          "other"
                        ],
                        "type": "string"
                      },
                      "subcategory": {
                        "description": "OpenOOH child slug (e.g. \"hotels\"). Free text on write; resolved on read.",
                        "type": "string"
                      }
                    },
                    "type": "object"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "created_at": "2026-07-14T18:03:22.401Z",
                    "device_id": "6624f1a0c3d4e5f6a7b8c9d0",
                    "device_type": "digital_signage",
                    "display": {
                      "height": 1920,
                      "orientation": "portrait",
                      "ppi": 96,
                      "width": 1080
                    },
                    "embed_url": "https://screen.trillboards.com?fp=P_a1b2c3d4e5f6a7b8c9d0e1f2",
                    "enrichment": {
                      "confidence": 0.92,
                      "status": "complete",
                      "venue_category": "retail"
                    },
                    "external_device_id": "lobby-nyc-001",
                    "fingerprint": "P_a1b2c3d4e5f6a7b8c9d0e1f2",
                    "location": {
                      "address": "1 Example Plaza, New York, NY 10004, USA",
                      "city": "New York",
                      "country": "US",
                      "lat": 40.7128,
                      "lng": -74.006,
                      "state": "NY",
                      "timezone": "America/New_York",
                      "venue_name": "Northwind Retail Broadway Lobby (North Wall)",
                      "venue_type": "malls",
                      "zip": "10004"
                    },
                    "location_capability": {
                      "position": "precise",
                      "rails": {
                        "geo_targeted_demand": {
                          "location_source": "screen",
                          "reason": null,
                          "status": "ready"
                        },
                        "inventory_feed": {
                          "location_source": "screen",
                          "reason": null,
                          "status": "ready"
                        },
                        "visit_attribution": {
                          "location_source": "screen",
                          "reason": null,
                          "status": "ready"
                        }
                      }
                    },
                    "monetization_status": "ready",
                    "name": "Northwind Retail Broadway Lobby (North Wall)",
                    "requirements": {
                      "currently_due": [],
                      "eventually_due": []
                    },
                    "screen_id": "6624f1a0c3d4e5f6a7b8ca11",
                    "status": "approved",
                    "updated_at": "2026-08-28T11:52:06.219Z",
                    "venue": {
                      "category": "retail",
                      "environment_type": "indoor",
                      "openooh_venue_type_id": 205,
                      "placement_type": "digital_signage",
                      "subcategory": "malls"
                    }
                  },
                  "message": "Device updated",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Device"
                    },
                    "message": {
                      "example": "Device updated",
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Device updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update device metadata",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/device/<deviceId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device_type\":\"tv\",\"mobility_type\":\"static\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"device_type\": \"tv\",\n    \"mobility_type\": \"static\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"device_type\": \"tv\",\n        \"mobility_type\": \"static\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'device_type' => 'tv',\n        'mobility_type' => 'static',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}/ads": {
      "get": {
        "description": "**SDK Endpoint** - Returns ads for the specified device.\n\nThis endpoint is called by the Trillboards Lite SDK. It supports ETag caching\nfor SDK ad-catalog responses to minimize bandwidth usage and request fan-out.\nBrowser language is accepted through the optional `lang` query parameter. If\nomitted, the server uses the standard `Accept-Language` request header and\nemits its ISO-639-1 alpha-2 primary language in the bidder-facing request.\n\nIf header bidding is enabled for the screen, the response includes\n`header_bidding_settings` with dynamic VAST waterfall URLs for programmatic\nplayback. Dynamic waterfall catalog responses intentionally preserve the\nfive-minute SDK cache contract; clients fetch the returned VAST URLs when\nthey actually attempt playback. Server-side source health may calculate\nrequest-pressure decisions for first-party upstream fetches, but the\ncatalog keeps SQL/Admin-enabled demand sources eligible and visible.\n\nNo authentication required - device is identified by fingerprint or external device ID.\n\n**The response carries TWO independent demand rails.**\n\n**1. `data.ads[]` — DIRECT BOOKINGS ONLY. Never programmatic.**\n\nEverything in this array is a first-party booking against this specific screen:\na paid direct advertiser buy, a partner self-promo creative uploaded through the\npartner CMS, or the Trillboards instant-QR house ad. There is no DSP, no\nauction, and no bid in `ads[]`. If you are looking for programmatic demand and\nreading only this array, you will conclude we have none.\n\n**2. `data.header_bidding_settings.vast_waterfall.sources[]` — ALL programmatic\ndemand.**\n\nEvery DSP/SSP source lives here and nowhere else. **Walk the whole array in\n`priority` ASCENDING order** and stop at the first source that actually plays;\nadvance on every failure class (no-fill, empty VAST, zero-`<Ad>`, transport\nerror, per-source timeout, IMA load error, IMA playback error). `priority: 0`\nis real and valid, and gaps in the values are normal — treat `priority` as an\nopaque ascending sort key, never an index.\n\n`vast_tag_url` is **an alias of `sources[0].vast_url`**, present only for\nlegacy single-tag players. It is not \"the\" tag: a player that reads it and\nstops has skipped every other source in the waterfall. This is the single most\nexpensive integration bug we see.\n\nThe waterfall may also contain a `trillboards_direct` source. That is the same\ndirect booking as rail 1, bridged into VAST for BYO-IMA players — it is not\nprogrammatic demand.\n\n**Non-IMA / native players must fire their own tracking.**\n\nGoogle IMA SDK fires the VAST-embedded pixels for you. **A raw `<video>`\nwrapper, `video.js` without the VAST plugin, Shaka without the ads plugin, or\nany other custom player does not.** If you are not using IMA you must XML-parse\nthe VAST yourself and fire, per playback and exactly once each: every\n`<Impression>`, `<Tracking event=\"start\">`, the quartiles\n(`firstQuartile`/`midpoint`/`thirdQuartile`), `<Tracking event=\"complete\">`, and\n`<Error>` (with `[ERRORCODE]` substituted) on any load or playback failure.\nPreserve `<Error>` at every wrapper level of the chain.\n\nIf you skip this we have no record of the impression: no attribution, no\nearnings. Working `DOMParser` implementation and fire order:\n[Partner native IMA SDK guide §5b — If you're NOT using Google IMA SDK (raw VAST\nplayer)](https://github.com/trillboards/packages/blob/production/trillboard-api/docs/integrations/partner-native-ima-sdk.md#5b-if-youre-not-using-google-ima-sdk-raw-vast-player).\n\n**Testing the wire format.**\n\nAppend **`?test=1`** to force a guaranteed-fill test source into that one\nresponse (public alias `src_test`): a direct MP4 with no wrapper chain to\nresolve. No allowlist enrollment, per-request only — real devices polling\nwithout the flag are unaffected — and the creative is non-revenue. If `src_test`\ncompletes but real sources do not, your VAST handling is fine and the problem is\nin the partner response path.\n",
        "operationId": "getAdsForDevice",
        "parameters": [
          {
            "description": "Device fingerprint or external device ID",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "ETag from previous static direct-ad response. Dynamic programmatic waterfall responses ignore matching ETags and return 200 with a fresh body.",
            "in": "header",
            "name": "If-None-Match",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Set to `1` (or `true`) to force a guaranteed-fill diagnostic source\n(`trillboards_test_vast`, public alias `src_test`, `priority: 0`) into\nTHIS response's `vast_waterfall.sources[]`. A direct MP4 with no wrapper\nchain. Per-request only — no allowlist enrollment, and devices polling\nwithout the flag are unaffected. The creative is non-revenue: use it to\nprove your VAST handling end to end, never in production playback.\n",
            "in": "query",
            "name": "test",
            "required": false,
            "schema": {
              "enum": [
                "1",
                "true"
              ],
              "example": "1",
              "type": "string"
            }
          },
          {
            "description": "Actual render slot width in pixels (player truth)",
            "in": "query",
            "name": "slot_w",
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Actual render slot height in pixels (player truth)",
            "in": "query",
            "name": "slot_h",
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Slot orientation (portrait or landscape)",
            "in": "query",
            "name": "orientation",
            "schema": {
              "enum": [
                "portrait",
                "landscape"
              ],
              "type": "string"
            }
          },
          {
            "description": "Whether player is muted (true/false or 1/0)",
            "in": "query",
            "name": "muted",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "Whether autoplay is allowed (true/false or 1/0)",
            "in": "query",
            "name": "autoplay",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "Runtime user agent string from the WebView/browser. Used for CTV device\ndetection when stored device metadata is missing. Enables correct dth\n(device type hint) in VAST tags — e.g., Fire TV Silk WebView UA triggers\ndth=4 (CTV) instead of defaulting to dth=2 (desktop).\n",
            "in": "query",
            "name": "ua",
            "schema": {
              "example": "Mozilla/5.0 (Linux; Android 9; AFTKA Build/PS7629) AppleWebKit/537.36 Silk/3.23",
              "type": "string"
            }
          },
          {
            "description": "Runtime browser/player locale. BCP 47 values such as `en-US` are\naccepted and normalized to the ISO-639-1 alpha-2 `device.language`\nvalue required by OpenRTB 2.6. When omitted, the standard\n`Accept-Language` header is used.\n",
            "in": "query",
            "name": "lang",
            "required": false,
            "schema": {
              "example": "en-US",
              "pattern": "^[A-Za-z]{2}(?:[-_](?:[A-Za-z]{2,8}|[0-9]{3}))*$",
              "type": "string"
            }
          },
          {
            "description": "Inventory Partner Domain for Google Ad Manager VAST requests. This must be\na single root domain with no scheme, path, query, or port, and it must match\nan `inventorypartnerdomain=<domain>` declaration in the relevant ads.txt or\napp-ads.txt inventory-sharing relationship. Only send this when instructed by\nTrillboards; the ad server may also append `ipd=trillboards.com` for scoped\nrevenue experiments.\n",
            "in": "query",
            "name": "ipd",
            "schema": {
              "example": "trillboards.com",
              "pattern": "^[A-Za-z0-9.-]+$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "ads": [
                      {
                        "allocation_id": "6507a1b2c3d4e5f6g7h8aa21",
                        "duration": 15,
                        "id": "507f1f77bcf86cd799439011",
                        "impression_id": "imp_1766644800000_abcd1234",
                        "impression_url": "https://api.trillboards.com/v1/partner/impression?adid=507f1f77bcf86cd799439011&impid=imp_1766644800000_abcd1234&did=6507a1b2c3d4e5f6g7h8aa01&sid=6507a1b2c3d4e5f6g7h8aa11&aid=6507a1b2c3d4e5f6g7h8aa21",
                        "title": "Summer Sale 50% Off",
                        "tracking": {
                          "complete_url": "https://api.trillboards.com/v1/partner/impression?adid=507f1f77bcf86cd799439011&impid=imp_1766644800000_abcd1234&did=6507a1b2c3d4e5f6g7h8aa01&sid=6507a1b2c3d4e5f6g7h8aa11&aid=6507a1b2c3d4e5f6g7h8aa21&completed=true",
                          "impression_url": "https://api.trillboards.com/v1/partner/impression?adid=507f1f77bcf86cd799439011&impid=imp_1766644800000_abcd1234&did=6507a1b2c3d4e5f6g7h8aa01&sid=6507a1b2c3d4e5f6g7h8aa11&aid=6507a1b2c3d4e5f6g7h8aa21",
                          "owned_direct_complete_url": "https://api.trillboards.com/openrtb/v1/event?event=complete&ingestion_path=owned_direct&adid=507f1f77bcf86cd799439011&screenId=6507a1b2c3d4e5f6g7h8aa11&deviceId=P_a1b2c3d4e5f6a7b8c9d0e1f2&src=owned_direct&variant_name=paid_direct&request_id=imp_1766644800000_abcd1234&sig=4b9ffbc2c7dd0a1e2f3b4c5d6e7f8a9b&impid=imp_1766644800000_abcd1234"
                        },
                        "type": "image",
                        "url": "https://cdn.trillboards.com/ads/summer-sale.jpg"
                      },
                      {
                        "allocation_id": "6507a1b2c3d4e5f6g7h8aa22",
                        "duration": 30,
                        "id": "507f1f77bcf86cd799439012",
                        "impression_id": "imp_1766644801000_dcba4321",
                        "impression_url": "https://api.trillboards.com/v1/partner/impression?adid=507f1f77bcf86cd799439012&impid=imp_1766644801000_dcba4321&did=6507a1b2c3d4e5f6g7h8aa01&sid=6507a1b2c3d4e5f6g7h8aa11&aid=6507a1b2c3d4e5f6g7h8aa22",
                        "title": "New Product Launch",
                        "type": "video",
                        "url": "https://cdn.trillboards.com/ads/product-launch.mp4"
                      }
                    ],
                    "cache_until": "2026-01-01T03:00:00.000Z",
                    "device": {
                      "display": {
                        "height": 1080,
                        "width": 1920
                      },
                      "fingerprint": "P_a1b2c3d4e5f6"
                    },
                    "header_bidding_settings": {
                      "allocation": 1,
                      "enabled": true,
                      "force_request": true,
                      "min_interval_seconds": 60,
                      "prebid": null,
                      "provider": "floodgates",
                      "request_mode": "always",
                      "variant_name": "simple",
                      "vast_tag_url": "https://pubads.g.doubleclick.net/gampad/ads?...&correlator=123"
                    },
                    "screen_dimensions": {
                      "height": 1080,
                      "orientation": "landscape",
                      "width": 1920
                    },
                    "screen_id": "6507a1b2c3d4e5f6g7h8aa11",
                    "screen_orientation": "landscape",
                    "settings": {
                      "ad_interval": 300,
                      "image_duration": 15,
                      "max_ads_per_hour": 20,
                      "overlay_button_text": "TAP TO CONTINUE",
                      "sound_enabled": false
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "ads": {
                          "items": {
                            "$ref": "#/components/schemas/Ad"
                          },
                          "type": "array"
                        },
                        "cache_until": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "commands": {
                          "description": "Pending device commands to execute (remote management).",
                          "items": {
                            "properties": {
                              "command": {
                                "type": "string"
                              },
                              "id": {
                                "type": "string"
                              },
                              "payload": {
                                "additionalProperties": true,
                                "type": "object"
                              },
                              "queued_at": {
                                "format": "date-time",
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "content_keywords": {
                          "description": "Venue/content keywords for contextual targeting. Omitted from the response when empty.",
                          "items": {
                            "type": "string"
                          },
                          "type": "array"
                        },
                        "device": {
                          "properties": {
                            "display": {
                              "properties": {
                                "height": {
                                  "nullable": true,
                                  "type": "integer"
                                },
                                "width": {
                                  "nullable": true,
                                  "type": "integer"
                                }
                              },
                              "type": "object"
                            },
                            "fingerprint": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "dooh_signals": {
                          "additionalProperties": true,
                          "description": "DOOH context signals (venue type, geo, audience) forwarded to demand.",
                          "type": "object"
                        },
                        "header_bidding_settings": {
                          "$ref": "#/components/schemas/HeaderBiddingSettings"
                        },
                        "ima_player_url": {
                          "description": "URL of the Trillboards-hosted IMA renderer for this screen, or null when the IMA player is not enabled for this deployment / there is no waterfall source.",
                          "format": "uri",
                          "nullable": true,
                          "type": "string"
                        },
                        "screen_dimensions": {
                          "properties": {
                            "height": {
                              "type": "integer"
                            },
                            "orientation": {
                              "enum": [
                                "portrait",
                                "landscape"
                              ],
                              "type": "string"
                            },
                            "width": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "screen_id": {
                          "type": "string"
                        },
                        "screen_locale": {
                          "description": "Serve locale derived from the venue country (e.g. \"en\", \"fr\").",
                          "type": "string"
                        },
                        "screen_name": {
                          "nullable": true,
                          "type": "string"
                        },
                        "screen_orientation": {
                          "enum": [
                            "portrait",
                            "landscape"
                          ],
                          "type": "string"
                        },
                        "settings": {
                          "$ref": "#/components/schemas/AdSettings"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Ads list",
            "headers": {
              "Cache-Control": {
                "description": "Static direct-ad responses use `private, max-age=300`; dynamic programmatic waterfall responses use `no-store, no-cache, must-revalidate, private`.",
                "schema": {
                  "type": "string"
                }
              },
              "ETag": {
                "description": "Use in If-None-Match for static direct-ad caching. Dynamic programmatic waterfall responses still emit an ETag for diagnostics but do not short-circuit to 304.",
                "schema": {
                  "example": "\"abc123def456\"",
                  "type": "string"
                }
              },
              "X-Ads-Unification-Mode": {
                "description": "Direct-ad unification canary mode for this request (`off`, `shadow`, or `serve`). Programmatic waterfall decisions remain independent of this header.",
                "schema": {
                  "enum": [
                    "off",
                    "shadow",
                    "serve"
                  ],
                  "type": "string"
                }
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "304": {
            "description": "Not Modified for static direct-ad responses only. Dynamic programmatic waterfall responses always return 200 with a fresh body."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [],
        "summary": "Get ads for device",
        "tags": [
          "Ads"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/device/<deviceId>/ads\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/ads');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/ads\",\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/ads');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}/commands/{commandId}/ack": {
      "post": {
        "description": "Acknowledges receipt and execution of a command sent to a device.\nTypically called by the SDK/device after processing a fleet command.\n",
        "operationId": "ackDeviceCommand",
        "parameters": [
          {
            "description": "Device identifier (fingerprint or external ID)",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Command ID to acknowledge",
            "in": "path",
            "name": "commandId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "result": {
                    "description": "Command execution result",
                    "type": "object"
                  },
                  "status": {
                    "default": "acked",
                    "description": "Acknowledgment status",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "message": "Command acknowledged",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "message": {
                      "example": "Command acknowledged",
                      "type": "string"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Command acknowledged"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Command does not belong to this device"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Device or command not found"
          }
        },
        "security": [],
        "summary": "Acknowledge a device command",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/device/<deviceId>/commands/<commandId>/ack\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"status\":\"acked\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/commands/<commandId>/ack', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"status\": \"acked\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/commands/<commandId>/ack\",\n    json={\n        \"status\": \"acked\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/commands/<commandId>/ack');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'status' => 'acked',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}/heartbeat": {
      "get": {
        "description": "Read-side companion to `POST /device/{deviceId}/heartbeat`. Returns\nthe device twin (Redis-cached, PG-fallback) so ad-serving partners\ncan check device health before issuing ad requests. Field-level\naccess control strips MDM compliance, vertex embeddings, identity-\ncorrection internals, partner identifiers, and Mongo `_id`. Pure\nread; no side effects.\n\nAuthenticated via the partner API key (`Authorization: Bearer\ntrb_partner_xxx` or `tb_prt_xxx`). Rate limit: 1000 req/min per\nAPI key.\n",
        "operationId": "getDeviceHeartbeat",
        "parameters": [
          {
            "description": "Device fingerprint or external device ID",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "adDeliveryProfile": {
                      "androidApiLevel": 28,
                      "autoRecoveryEnabled": true,
                      "imaIntegration": "html5_webview",
                      "mode": "ima_sdk",
                      "notes": [
                        "Chrome <87: content restoration degraded after WebView suspend/resume. ContentHealthMonitor active on client."
                      ],
                      "reason": "default",
                      "webviewChromiumMajor": 84
                    },
                    "deviceId": "P_a1b2c3d4e5f6a7b8c9d0e1f2",
                    "lastHeartbeatAt": "2026-08-28T11:55:04.512Z",
                    "measurementMetadata": {
                      "ageSeconds": 42,
                      "cachedAt": null,
                      "receivedAt": "2026-08-28T11:55:04.512Z",
                      "source": "pg_fallback"
                    },
                    "stalenessSeconds": 42,
                    "telemetry": {
                      "adDeliveryMode": "ima_sdk",
                      "adDeliveryReason": "default",
                      "agentStatus": "healthy",
                      "androidApiLevel": 28,
                      "brightness": 0.8,
                      "consecutiveAdFailures": 0,
                      "imaIntegration": "html5_webview",
                      "imaSupported": true,
                      "muted": false,
                      "networkStrengthDbm": -58,
                      "powerState": "on",
                      "temperatureC": 41.5,
                      "uptimeSeconds": 431520,
                      "visibilityState": "visible",
                      "volume": 0.25,
                      "webviewChromiumMajor": 84
                    }
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/PartnerHeartbeatQueryResponse"
                }
              }
            },
            "description": "Live device heartbeat state."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Device does not belong to this partner"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read live device heartbeat state",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/device/<deviceId>/heartbeat\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/heartbeat', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/heartbeat\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/heartbeat');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "description": "Mark a device online and record `lastHeartbeatAt`.\n\nThe embeddable agent-core Sensing SDK and CTV Measurement SDK send this\nautomatically on their configured heartbeat cadence. Custom players\nmay POST the same payload themselves.\n`:deviceId` accepts your `external_id` (the same value you registered)\nor the device fingerprint. Read the result back from\n`GET /device/{deviceId}/heartbeat` — `lastHeartbeatAt` is NOT included\non the plain `GET /device/{deviceId}` response.\n\nSupports structured telemetry payloads (`telemetry`, `sdk`, `device`, `network`, `ad`)\nand legacy flat heartbeat keys for backward compatibility.\n\nThe request body accepts legacy flat/nested telemetry\n(`PartnerHeartbeatRequest`), agent-core sensing fields, or both together.\nThe sensing schema (`PartnerDeviceHeartbeatBody`) describes which\nsensing fields actually ship in production (BLE, WiFi, mDNS, SSDP,\nHTTP probes, skip-reason counts) and explicitly excludes stub fields\nwith 0% production population (Phase 6 UWB / Auracast / Channel\nSounding, MDM enrollment, native sensors, CSI).\n\nThis registered-device heartbeat is intentionally unauthenticated.\nLiveness, `advertising_identity`, and `extended_identities` use the same\ndevice binding: `deviceId` must resolve to a device that was registered\nalready, and every identity envelope must pass the typed validation\nbelow. No partner API key or device JWT is required.\n",
        "operationId": "heartbeat",
        "parameters": [
          {
            "description": "Device fingerprint or external device ID",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/PartnerHeartbeatRequest"
                  },
                  {
                    "$ref": "#/components/schemas/PartnerDeviceHeartbeatBody"
                  }
                ]
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "ad_delivery_profile": {
                      "android_api_level": 28,
                      "auto_recovery_enabled": true,
                      "ima_integration": "html5_webview",
                      "mode": "ima_sdk",
                      "notes": [
                        "Chrome <87: content restoration degraded after WebView suspend/resume. ContentHealthMonitor active on client."
                      ],
                      "reason": "default",
                      "webview_chromium_major": 84
                    },
                    "commands": [
                      {
                        "command": "refresh_ads",
                        "id": "6624f2b7c3d4e5f6a7b8cd01",
                        "payload": {},
                        "queued_at": "2026-08-28T11:54:38.006Z"
                      }
                    ],
                    "last_seen_at": "2026-08-28T11:55:04.512Z",
                    "status": "online"
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/PartnerHeartbeatResponse"
                }
              }
            },
            "description": "Heartbeat recorded"
          }
        },
        "security": [],
        "summary": "Device heartbeat",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/device/<deviceId>/heartbeat\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"power_state\":\"on\",\"ima_integration\":\"android_native\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/heartbeat', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"power_state\": \"on\",\n    \"ima_integration\": \"android_native\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/heartbeat\",\n    json={\n        \"power_state\": \"on\",\n        \"ima_integration\": \"android_native\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/heartbeat');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'power_state' => 'on',\n        'ima_integration' => 'android_native',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}/impression-multiplier": {
      "patch": {
        "description": "Update the existing screen's publisher-declared audience impressions per ad play.\nAuthenticate with the owning partner API key. The device and linked screen must\nbe active records owned by that partner; another partner's device returns 404.\nThis writes only the screen multiplier and its update timestamp, preserving its\ndevice IDs and other settings. Repeat requests are idempotent.\n\nSend a JSON number between 0.1 and 100 inclusive, matching the venue-screen\neditor's range. Values are stored to four decimal places. Use the measurement\nprovider's value for the applicable spot duration; a daily traffic total is a\ndifferent unit. Repeat this call for each device in your fleet.\n\nThe saved value is returned here and in GET /v1/partner/device/{deviceId}\nand GET /v1/partner/devices. It feeds the existing publisher-estimate inventory\nand uncurated bid paths; measured quantity and billing keep their existing precedence.\n",
        "operationId": "updateDeviceImpressionMultiplier",
        "parameters": [
          {
            "description": "Trillboards device ID, fingerprint, or URL-encoded external_device_id",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "additionalProperties": false,
                "properties": {
                  "impression_multiplier": {
                    "description": "Publisher-declared audience impressions per ad play; stored to four decimal places",
                    "example": 2.4,
                    "maximum": 100,
                    "minimum": 0.1,
                    "type": "number"
                  }
                },
                "required": [
                  "impression_multiplier"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "device_id": "6624f1a0c3d4e5f6a7b8c9d0",
                    "external_device_id": "lobby-nyc-001",
                    "impression_multiplier": 2.4,
                    "screen_id": "6624f1a0c3d4e5f6a7b8ca11"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "device_id": {
                          "type": "string"
                        },
                        "external_device_id": {
                          "type": "string"
                        },
                        "impression_multiplier": {
                          "type": "number"
                        },
                        "screen_id": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Saved multiplier, returned as a JSON number"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Missing, nonnumeric, out-of-range multiplier, or unexpected body fields"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Missing or invalid partner API key"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Device or linked screen not found in the authenticated partner's active records"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update a device's publisher impression multiplier",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/device/<deviceId>/impression-multiplier\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"impression_multiplier\":2.4}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/impression-multiplier', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"impression_multiplier\": 2.4\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/impression-multiplier\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"impression_multiplier\": 2.4,\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/impression-multiplier');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'impression_multiplier' => 2.4,\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}/interactions": {
      "post": {
        "description": "The partner SDK's `reportInteraction` host hook: what a rider did with this\ndevice's screen — a tap, a QR code shown or scanned, an attention prompt — with\nthe creative on screen and how far into it. Stored for measurement only; never\nbilled, and never an ad-lifecycle event (those go to\n`POST /v1/partner/device/{deviceId}/programmatic-event`).\n\nAuthenticated with the device token the SDK holds. The screen is this device's\nown binding and the partner is that screen's partner; the body may not name a\nscreen.\n\nIdempotent per device on `interaction_id`: a retried interaction is answered\n`duplicate` and stored once, and two devices that use the same id report two\ninteractions.\n\n`occurred_at` is the device clock and must lie within the last 7 days and at most\n5 minutes ahead of the server clock; any other time is refused with 400.\n",
        "operationId": "recordPartnerDeviceInteraction",
        "parameters": [
          {
            "description": "This device, as the SDK addresses it — the external id it was registered with, its canonical fingerprint, or its device id. Must be the device the token was issued to.",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Request time in epoch milliseconds; must be within the device replay window.",
            "in": "header",
            "name": "X-Device-Timestamp",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "A fresh value per request (the SDK sends a UUID); a reused nonce is refused 409.",
            "in": "header",
            "name": "X-Device-Nonce",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "creative_id": "cr_7731",
                "interaction_id": "int_5d21",
                "occurred_at": "2026-09-20T12:00:08.000Z",
                "t_ms": 8000,
                "type": "tap"
              },
              "schema": {
                "$ref": "#/components/schemas/PartnerInteraction"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "interaction_id": "int_5d21",
                    "screen_id": "6a0000000000000000000a01",
                    "status": "accepted",
                    "type": "tap"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "interaction_id": {
                          "type": "string"
                        },
                        "screen_id": {
                          "description": "The screen this device is bound to.",
                          "type": "string"
                        },
                        "status": {
                          "enum": [
                            "accepted",
                            "duplicate"
                          ],
                          "type": "string"
                        },
                        "type": {
                          "enum": [
                            "tap",
                            "qr_shown",
                            "qr_scanned",
                            "dwell_prompt",
                            "seatbelt_reminder_shown"
                          ],
                          "type": "string"
                        }
                      },
                      "required": [
                        "interaction_id",
                        "type",
                        "screen_id",
                        "status"
                      ],
                      "type": "object"
                    },
                    "success": {
                      "enum": [
                        true
                      ],
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Interaction accepted, or a duplicate of this device's prior interaction_id."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": 401,
                  "error": "device_token_required",
                  "message": "X-Device-Token header is required",
                  "status": false
                },
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "No device token, a token we did not issue, or a token for no device."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "$ref": "#/components/schemas/DeviceAuthRefusal"
                    }
                  ]
                }
              }
            },
            "description": "Refused after authentication: the path names another device\n(`device_token_path_mismatch`), the device is bound to no screen\n(`device_not_bound_to_screen`), the screen belongs to no partner\n(`screen_not_partner_bound`) or the partner is not active (`partner_inactive`);\nor the device credential does not match its request (`DeviceAuthRefusal`).\n"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "This X-Device-Nonce was already used (a replayed request). Retry the interaction with a new nonce."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "The replay guard is unavailable; retry."
          }
        },
        "security": [
          {
            "DeviceToken": []
          }
        ],
        "summary": "Report a rider interaction from the partner SDK (reportInteraction)",
        "tags": [
          "SDK Host Hooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/device/<deviceId>/interactions\" \\\n  -H \"X-Device-Token: $TRILLBOARDS_DEVICE_TOKEN\" \\\n  -H \"X-Device-Timestamp: <X-Device-Timestamp>\" \\\n  -H \"X-Device-Nonce: <X-Device-Nonce>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"interaction_id\":\"int_5d21\",\"type\":\"tap\",\"occurred_at\":\"2026-09-20T12:00:08.000Z\",\"creative_id\":\"cr_7731\",\"t_ms\":8000}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/interactions', {\n  method: 'POST',\n  headers: {\n    'X-Device-Token': `${process.env.TRILLBOARDS_DEVICE_TOKEN}`,\n    'X-Device-Timestamp': \"<X-Device-Timestamp>\",\n    'X-Device-Nonce': \"<X-Device-Nonce>\",\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"interaction_id\": \"int_5d21\",\n    \"type\": \"tap\",\n    \"occurred_at\": \"2026-09-20T12:00:08.000Z\",\n    \"creative_id\": \"cr_7731\",\n    \"t_ms\": 8000\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/interactions\",\n    headers={\n        \"X-Device-Token\": f\"{os.environ['TRILLBOARDS_DEVICE_TOKEN']}\",\n        \"X-Device-Timestamp\": \"<X-Device-Timestamp>\",\n        \"X-Device-Nonce\": \"<X-Device-Nonce>\",\n    },\n    json={\n        \"interaction_id\": \"int_5d21\",\n        \"type\": \"tap\",\n        \"occurred_at\": \"2026-09-20T12:00:08.000Z\",\n        \"creative_id\": \"cr_7731\",\n        \"t_ms\": 8000,\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/interactions');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'X-Device-Token: ' . getenv('TRILLBOARDS_DEVICE_TOKEN'),\n        'X-Device-Timestamp: <X-Device-Timestamp>',\n        'X-Device-Nonce: <X-Device-Nonce>',\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'interaction_id' => 'int_5d21',\n        'type' => 'tap',\n        'occurred_at' => '2026-09-20T12:00:08.000Z',\n        'creative_id' => 'cr_7731',\n        't_ms' => 8000,\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}/plays": {
      "post": {
        "description": "The partner SDK's `reportPlay` host hook: one log-level play of an ad the host\napp played on this device's screen, into the same non-billing store as\n`POST /v1/partner/plays` — the same body without `screen_id`, plus the\nTrillboards `impression_id` when the host played an ad Trillboards served.\n\nAuthenticated with the device token the SDK holds, never a partner API key.\nThe screen is this device's own binding and the partner is that screen's\npartner, so the body may not name a screen: a body carrying `screen_id`, or any\nfield not listed, is refused with 400.\n\nIdempotent on `play_id`, shared with `POST /v1/partner/plays`, so a play\nreported through both is one play. When `impression_id` matches a\nTrillboards-served play on the same screen, on the same or an adjacent UTC day,\nor, without one, the same `ad_id` completes within 5 s of one, the Trillboards\nrecord of that play is kept and this report is not counted a second time.\nMeasurement plays are never billed.\n\n`completed_at` is the device clock and must lie within the last 7 days and at\nmost 5 minutes ahead of the server clock; any other time is refused with 400.\n",
        "operationId": "recordPartnerDevicePlay",
        "parameters": [
          {
            "description": "This device, as the SDK addresses it — the external id it was registered with, its canonical fingerprint, or its device id. Must be the device the token was issued to.",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Request time in epoch milliseconds; must be within the device replay window.",
            "in": "header",
            "name": "X-Device-Timestamp",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "A fresh value per request (the SDK sends a UUID); a reused nonce is refused 409.",
            "in": "header",
            "name": "X-Device-Nonce",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "completed_at": "2026-09-20T12:00:15.000Z",
                "creative_ref": "sha256:6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1",
                "duration_seconds": 15,
                "impression_id": "trb_6a5f0c2e_0",
                "play_id": "play_20260920T120015Z_7f3a"
              },
              "schema": {
                "$ref": "#/components/schemas/PartnerDevicePlay"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "content_sha256": "6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1",
                    "play_id": "play_20260920T120015Z_7f3a",
                    "screen_id": "6a0000000000000000000a01",
                    "status": "accepted"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "content_sha256": {
                          "type": "string"
                        },
                        "play_id": {
                          "type": "string"
                        },
                        "screen_id": {
                          "description": "The screen this device is bound to.",
                          "type": "string"
                        },
                        "status": {
                          "enum": [
                            "accepted",
                            "duplicate"
                          ],
                          "type": "string"
                        }
                      },
                      "required": [
                        "play_id",
                        "screen_id",
                        "content_sha256",
                        "status"
                      ],
                      "type": "object"
                    },
                    "success": {
                      "enum": [
                        true
                      ],
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Play accepted, or a duplicate of a prior play_id."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": 401,
                  "error": "device_token_required",
                  "message": "X-Device-Token header is required",
                  "status": false
                },
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "No device token, a token we did not issue, or a token for no device."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "$ref": "#/components/schemas/DeviceAuthRefusal"
                    }
                  ]
                }
              }
            },
            "description": "Refused after authentication: the path names another device\n(`device_token_path_mismatch`), the device is bound to no screen\n(`device_not_bound_to_screen`), the screen belongs to no partner\n(`screen_not_partner_bound`) or the partner is not active (`partner_inactive`);\nor the device credential does not match its request (`DeviceAuthRefusal`).\n"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "This X-Device-Nonce was already used (a replayed request). Retry the play with a new nonce."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "The replay guard is unavailable; retry."
          }
        },
        "security": [
          {
            "DeviceToken": []
          }
        ],
        "summary": "Report a play from the partner SDK (reportPlay)",
        "tags": [
          "SDK Host Hooks",
          "Measurement Plays"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/device/<deviceId>/plays\" \\\n  -H \"X-Device-Token: $TRILLBOARDS_DEVICE_TOKEN\" \\\n  -H \"X-Device-Timestamp: <X-Device-Timestamp>\" \\\n  -H \"X-Device-Nonce: <X-Device-Nonce>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"creative_ref\":\"sha256:6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1\",\"play_id\":\"play_20260920T120015Z_7f3a\",\"completed_at\":\"2026-09-20T12:00:15.000Z\",\"duration_seconds\":15,\"impression_id\":\"trb_6a5f0c2e_0\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/plays', {\n  method: 'POST',\n  headers: {\n    'X-Device-Token': `${process.env.TRILLBOARDS_DEVICE_TOKEN}`,\n    'X-Device-Timestamp': \"<X-Device-Timestamp>\",\n    'X-Device-Nonce': \"<X-Device-Nonce>\",\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"creative_ref\": \"sha256:6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1\",\n    \"play_id\": \"play_20260920T120015Z_7f3a\",\n    \"completed_at\": \"2026-09-20T12:00:15.000Z\",\n    \"duration_seconds\": 15,\n    \"impression_id\": \"trb_6a5f0c2e_0\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/plays\",\n    headers={\n        \"X-Device-Token\": f\"{os.environ['TRILLBOARDS_DEVICE_TOKEN']}\",\n        \"X-Device-Timestamp\": \"<X-Device-Timestamp>\",\n        \"X-Device-Nonce\": \"<X-Device-Nonce>\",\n    },\n    json={\n        \"creative_ref\": \"sha256:6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1\",\n        \"play_id\": \"play_20260920T120015Z_7f3a\",\n        \"completed_at\": \"2026-09-20T12:00:15.000Z\",\n        \"duration_seconds\": 15,\n        \"impression_id\": \"trb_6a5f0c2e_0\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/plays');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'X-Device-Token: ' . getenv('TRILLBOARDS_DEVICE_TOKEN'),\n        'X-Device-Timestamp: <X-Device-Timestamp>',\n        'X-Device-Nonce: <X-Device-Nonce>',\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'creative_ref' => 'sha256:6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1',\n        'play_id' => 'play_20260920T120015Z_7f3a',\n        'completed_at' => '2026-09-20T12:00:15.000Z',\n        'duration_seconds' => 15,\n        'impression_id' => 'trb_6a5f0c2e_0',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}/programmatic-event": {
      "post": {
        "description": "**SDK Analytics Endpoint** - Records programmatic ad lifecycle events for\nTrillboards' internal analytics and triggers partner webhooks when subscribed.\n\n**Important**: This endpoint is for **server-side event recording only**.\nIt does NOT push events to your native Android, iOS, or CTV applications.\n\nFor receiving real-time events in native applications, use the\n**JavaScript Native Bridge Protocol** instead. The SDK automatically sends\nevents to standard native interfaces (Android WebView, iOS WKWebView,\nReact Native, Flutter, etc.).\n\nSee documentation: https://api.trillboards.com/docs/integrations/partner-native-bridge.md\n\n### Events Recorded:\n- `ad_started` - Ad began playing\n- `ad_ended` - Ad finished (complete or skipped)\n- `no_fill` - No programmatic ad available\n- `error` - Ad playback error\n",
        "operationId": "recordProgrammaticEvent",
        "parameters": [
          {
            "description": "Device fingerprint or external device ID",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "ad_id": {
                    "type": "string"
                  },
                  "error_code": {
                    "type": "string"
                  },
                  "error_message": {
                    "type": "string"
                  },
                  "event": {
                    "enum": [
                      "ad_started",
                      "ad_ended",
                      "no_fill",
                      "error"
                    ],
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "request_id": {
                    "type": "string"
                  },
                  "screen_orientation": {
                    "enum": [
                      "portrait",
                      "landscape"
                    ],
                    "type": "string"
                  },
                  "slot_height": {
                    "type": "integer"
                  },
                  "slot_width": {
                    "type": "integer"
                  },
                  "variant_name": {
                    "type": "string"
                  }
                },
                "required": [
                  "event"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "success": true
                },
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Event recorded"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [],
        "summary": "Record programmatic ad event (Server-side Analytics)",
        "tags": [
          "Ads"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/device/<deviceId>/programmatic-event\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event\":\"ad_started\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/programmatic-event', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"event\": \"ad_started\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/programmatic-event\",\n    json={\n        \"event\": \"ad_started\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/programmatic-event');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'event' => 'ad_started',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/device/{deviceId}/trips": {
      "post": {
        "description": "The partner SDK's `setTripState` host hook: one meter state change — meter on or\noff, fare, tip, payment type, riders, destination — on this device's screen.\nStored for measurement only; never billed.\n\nAuthenticated with the device token the SDK holds. The screen is this device's\nown binding and the partner is that screen's partner; the body may not name a\nscreen.\n\nIdempotent per device on (`trip_id`, `event_seq`): `event_seq` increases within a\ntrip and a retry re-sends the same value, so a retried event is answered\n`duplicate` and stored once. Hosts number trips per vehicle, so two devices that\nboth report trip `1` report two trips.\n\n`state_at` is the device clock and must lie within the last 7 days and at most\n5 minutes ahead of the server clock; any other time is refused with 400.\n",
        "operationId": "recordPartnerDeviceTripState",
        "parameters": [
          {
            "description": "This device, as the SDK addresses it — the external id it was registered with, its canonical fingerprint, or its device id. Must be the device the token was issued to.",
            "in": "path",
            "name": "deviceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Request time in epoch milliseconds; must be within the device replay window.",
            "in": "header",
            "name": "X-Device-Timestamp",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "A fresh value per request (the SDK sends a UUID); a reused nonce is refused 409.",
            "in": "header",
            "name": "X-Device-Nonce",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "destination": "JFK Terminal 4",
                "event_seq": 1,
                "fare_cents": 2450,
                "meter_on": false,
                "passenger_count": 2,
                "payment_type": "card",
                "state_at": "2026-09-20T12:18:30.000Z",
                "tip_cents": 400,
                "trip_id": "trip_8841"
              },
              "schema": {
                "$ref": "#/components/schemas/PartnerTripState"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "event_seq": 1,
                    "screen_id": "6a0000000000000000000a01",
                    "status": "accepted",
                    "trip_id": "trip_8841"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "event_seq": {
                          "type": "integer"
                        },
                        "screen_id": {
                          "description": "The screen this device is bound to.",
                          "type": "string"
                        },
                        "status": {
                          "enum": [
                            "accepted",
                            "duplicate"
                          ],
                          "type": "string"
                        },
                        "trip_id": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "trip_id",
                        "event_seq",
                        "screen_id",
                        "status"
                      ],
                      "type": "object"
                    },
                    "success": {
                      "enum": [
                        true
                      ],
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Event accepted, or a duplicate of this device's prior (trip_id, event_seq)."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "content": {
              "application/json": {
                "example": {
                  "code": 401,
                  "error": "device_token_required",
                  "message": "X-Device-Token header is required",
                  "status": false
                },
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "No device token, a token we did not issue, or a token for no device."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "$ref": "#/components/schemas/DeviceAuthRefusal"
                    }
                  ]
                }
              }
            },
            "description": "Refused after authentication: the path names another device\n(`device_token_path_mismatch`), the device is bound to no screen\n(`device_not_bound_to_screen`), the screen belongs to no partner\n(`screen_not_partner_bound`) or the partner is not active (`partner_inactive`);\nor the device credential does not match its request (`DeviceAuthRefusal`).\n"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "This X-Device-Nonce was already used (a replayed request). Retry the event with a new nonce."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceAuthRefusal"
                }
              }
            },
            "description": "The replay guard is unavailable; retry."
          }
        },
        "security": [
          {
            "DeviceToken": []
          }
        ],
        "summary": "Report a taxi-meter state change from the partner SDK (setTripState)",
        "tags": [
          "SDK Host Hooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/device/<deviceId>/trips\" \\\n  -H \"X-Device-Token: $TRILLBOARDS_DEVICE_TOKEN\" \\\n  -H \"X-Device-Timestamp: <X-Device-Timestamp>\" \\\n  -H \"X-Device-Nonce: <X-Device-Nonce>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"trip_id\":\"trip_8841\",\"event_seq\":1,\"state_at\":\"2026-09-20T12:18:30.000Z\",\"meter_on\":false,\"fare_cents\":2450,\"tip_cents\":400,\"payment_type\":\"card\",\"passenger_count\":2,\"destination\":\"JFK Terminal 4\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/device/<deviceId>/trips', {\n  method: 'POST',\n  headers: {\n    'X-Device-Token': `${process.env.TRILLBOARDS_DEVICE_TOKEN}`,\n    'X-Device-Timestamp': \"<X-Device-Timestamp>\",\n    'X-Device-Nonce': \"<X-Device-Nonce>\",\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"trip_id\": \"trip_8841\",\n    \"event_seq\": 1,\n    \"state_at\": \"2026-09-20T12:18:30.000Z\",\n    \"meter_on\": false,\n    \"fare_cents\": 2450,\n    \"tip_cents\": 400,\n    \"payment_type\": \"card\",\n    \"passenger_count\": 2,\n    \"destination\": \"JFK Terminal 4\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/device/<deviceId>/trips\",\n    headers={\n        \"X-Device-Token\": f\"{os.environ['TRILLBOARDS_DEVICE_TOKEN']}\",\n        \"X-Device-Timestamp\": \"<X-Device-Timestamp>\",\n        \"X-Device-Nonce\": \"<X-Device-Nonce>\",\n    },\n    json={\n        \"trip_id\": \"trip_8841\",\n        \"event_seq\": 1,\n        \"state_at\": \"2026-09-20T12:18:30.000Z\",\n        \"meter_on\": False,\n        \"fare_cents\": 2450,\n        \"tip_cents\": 400,\n        \"payment_type\": \"card\",\n        \"passenger_count\": 2,\n        \"destination\": \"JFK Terminal 4\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/device/<deviceId>/trips');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'X-Device-Token: ' . getenv('TRILLBOARDS_DEVICE_TOKEN'),\n        'X-Device-Timestamp: <X-Device-Timestamp>',\n        'X-Device-Nonce: <X-Device-Nonce>',\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'trip_id' => 'trip_8841',\n        'event_seq' => 1,\n        'state_at' => '2026-09-20T12:18:30.000Z',\n        'meter_on' => false,\n        'fare_cents' => 2450,\n        'tip_cents' => 400,\n        'payment_type' => 'card',\n        'passenger_count' => 2,\n        'destination' => 'JFK Terminal 4',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/devices": {
      "get": {
        "operationId": "listDevices",
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "enum": [
                "pending",
                "active",
                "offline",
                "suspended"
              ],
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 100,
              "maximum": 500,
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "devices": [
                      {
                        "created_at": "2026-07-14T18:03:22.401Z",
                        "device_id": "6624f1a0c3d4e5f6a7b8c9d0",
                        "device_type": "digital_signage",
                        "display": {
                          "height": 1080,
                          "orientation": "landscape",
                          "ppi": 96,
                          "width": 1920
                        },
                        "embed_url": "https://screen.trillboards.com?fp=P_a1b2c3d4e5f6a7b8c9d0e1f2",
                        "enrichment": {
                          "confidence": 0.92,
                          "status": "complete",
                          "venue_category": "retail"
                        },
                        "external_device_id": "lobby-nyc-001",
                        "fingerprint": "P_a1b2c3d4e5f6a7b8c9d0e1f2",
                        "location": {
                          "address": "1 Example Plaza, New York, NY 10004, USA",
                          "city": "New York",
                          "country": "US",
                          "lat": 40.7128,
                          "lng": -74.006,
                          "state": "NY",
                          "timezone": "America/New_York",
                          "venue_name": "Northwind Retail Broadway Lobby",
                          "venue_type": "malls",
                          "zip": "10004"
                        },
                        "location_capability": {
                          "position": "precise",
                          "rails": {
                            "geo_targeted_demand": {
                              "location_source": "screen",
                              "reason": null,
                              "status": "ready"
                            },
                            "inventory_feed": {
                              "location_source": "screen",
                              "reason": null,
                              "status": "ready"
                            },
                            "visit_attribution": {
                              "location_source": "screen",
                              "reason": null,
                              "status": "ready"
                            }
                          }
                        },
                        "monetization_status": "ready",
                        "name": "Northwind Retail Broadway Lobby",
                        "requirements": {
                          "currently_due": [],
                          "eventually_due": []
                        },
                        "screen_id": "6624f1a0c3d4e5f6a7b8ca11",
                        "status": "approved",
                        "updated_at": "2026-08-28T09:12:47.883Z",
                        "venue": {
                          "category": "retail",
                          "environment_type": "indoor",
                          "openooh_venue_type_id": 205,
                          "placement_type": "digital_signage",
                          "subcategory": "malls"
                        }
                      }
                    ],
                    "limit": 100,
                    "offset": 0,
                    "total": 1
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "devices": {
                          "items": {
                            "$ref": "#/components/schemas/DeviceSummary"
                          },
                          "type": "array"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Device list"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List all devices",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/devices\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/devices', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/devices\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/devices');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "description": "Register — or idempotently re-register, keyed on `external_id` — a single partner device and provision its screen in one call. Returns the embeddable player URL and the server-derived device identity. Always send the device's real WebView `ua`: it is the single most important field for DSP biddability.\n",
        "operationId": "registerDeviceV2",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "display": {
                    "description": "Optional display dimensions. One of three modes — the `mode` discriminator is required.",
                    "oneOf": [
                      {
                        "properties": {
                          "height_in": {
                            "description": "Physical screen height in INCHES. Pairs with `width_in`.",
                            "type": "number"
                          },
                          "mode": {
                            "enum": [
                              "inches"
                            ],
                            "type": "string"
                          },
                          "width_in": {
                            "description": "Physical screen width in INCHES. No upper bound — large-format / billboard inventory is valid.",
                            "type": "number"
                          }
                        },
                        "required": [
                          "mode",
                          "width_in",
                          "height_in"
                        ],
                        "type": "object"
                      },
                      {
                        "properties": {
                          "height": {
                            "type": "integer"
                          },
                          "mode": {
                            "enum": [
                              "pixels_with_ppi"
                            ],
                            "type": "string"
                          },
                          "ppi": {
                            "type": "number"
                          },
                          "width": {
                            "type": "integer"
                          }
                        },
                        "required": [
                          "mode",
                          "width",
                          "height",
                          "ppi"
                        ],
                        "type": "object"
                      },
                      {
                        "properties": {
                          "height": {
                            "type": "integer"
                          },
                          "mode": {
                            "enum": [
                              "pixels_only"
                            ],
                            "type": "string"
                          },
                          "width": {
                            "type": "integer"
                          }
                        },
                        "required": [
                          "mode",
                          "width",
                          "height"
                        ],
                        "type": "object"
                      }
                    ]
                  },
                  "external_id": {
                    "description": "Your stable per-device identifier (e.g. \"REC00005\").",
                    "type": "string"
                  },
                  "hardware": {
                    "description": "Optional hardware identity — improves DSP device matching.",
                    "properties": {
                      "make": {
                        "type": "string"
                      },
                      "model": {
                        "type": "string"
                      },
                      "os": {
                        "enum": [
                          "Android",
                          "iOS",
                          "Tizen",
                          "WebOS",
                          "Linux",
                          "FireOS",
                          "Other"
                        ],
                        "type": "string"
                      },
                      "osv": {
                        "type": "string"
                      }
                    },
                    "type": "object"
                  },
                  "location": {
                    "description": "Optional venue location — either lat/lng coordinates or a street address.",
                    "oneOf": [
                      {
                        "properties": {
                          "lat": {
                            "maximum": 90,
                            "minimum": -90,
                            "type": "number"
                          },
                          "lng": {
                            "maximum": 180,
                            "minimum": -180,
                            "type": "number"
                          }
                        },
                        "required": [
                          "lat",
                          "lng"
                        ],
                        "type": "object"
                      },
                      {
                        "properties": {
                          "address": {
                            "maxLength": 200,
                            "minLength": 5,
                            "type": "string"
                          }
                        },
                        "required": [
                          "address"
                        ],
                        "type": "object"
                      }
                    ]
                  },
                  "ua": {
                    "description": "The device's real WebView User-Agent. Must include a Build fingerprint and be unique per device.",
                    "maxLength": 1024,
                    "minLength": 20,
                    "type": "string"
                  }
                },
                "required": [
                  "external_id",
                  "ua"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "device_id": "6624f1a0c3d4e5f6a7b8c9d0",
                    "diagnostics_url": "https://api.trillboards.com/v1/partner/devices/6624f1a0c3d4e5f6a7b8c9d0/diagnostics",
                    "embed_url": "https://screen.trillboards.com?fp=P_a1b2c3d4e5f6a7b8c9d0e1f2",
                    "identity": {
                      "derivation_confidence": 0.6,
                      "derivation_source": "ua_parser",
                      "device_class": "dooh_signage",
                      "make": "OTT Box",
                      "model": "X96S_V311_P",
                      "os": "Android",
                      "osv": "9"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "device_id": {
                          "type": "string"
                        },
                        "diagnostics_url": {
                          "format": "uri",
                          "type": "string"
                        },
                        "embed_url": {
                          "description": "Paste into an iframe to start serving.",
                          "format": "uri",
                          "type": "string"
                        },
                        "identity": {
                          "description": "Server-derived canonical device identity.",
                          "properties": {
                            "derivation_confidence": {
                              "nullable": true,
                              "type": "number"
                            },
                            "derivation_source": {
                              "nullable": true,
                              "type": "string"
                            },
                            "device_class": {
                              "nullable": true,
                              "type": "string"
                            },
                            "make": {
                              "nullable": true,
                              "type": "string"
                            },
                            "model": {
                              "nullable": true,
                              "type": "string"
                            },
                            "os": {
                              "nullable": true,
                              "type": "string"
                            },
                            "osv": {
                              "nullable": true,
                              "type": "string"
                            }
                          },
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Device registered (or idempotently re-registered)"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Register a device (V2 single-call onboarding)",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/devices\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"external_id\":\"<external_id>\",\"ua\":\"<ua>\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/devices', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"external_id\": \"<external_id>\",\n    \"ua\": \"<ua>\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/devices\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"external_id\": \"<external_id>\",\n        \"ua\": \"<ua>\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/devices');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'external_id' => '<external_id>',\n        'ua' => '<ua>',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/devices/batch": {
      "post": {
        "description": "Register up to 100 devices in a single request.\n\nEach device must include `device_id`. Optional `defaults` are applied to every device.\n",
        "operationId": "registerDevicesBatch",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "defaults": {
                    "description": "Default fields applied to each device",
                    "properties": {
                      "display_settings": {
                        "$ref": "#/components/schemas/PartnerRegistrationDisplaySettings"
                      }
                    },
                    "type": "object"
                  },
                  "devices": {
                    "items": {
                      "properties": {
                        "device_id": {
                          "type": "string"
                        },
                        "device_type": {
                          "type": "string"
                        },
                        "display": {
                          "type": "object"
                        },
                        "display_settings": {
                          "$ref": "#/components/schemas/PartnerRegistrationDisplaySettings"
                        },
                        "location": {
                          "type": "object"
                        },
                        "name": {
                          "type": "string"
                        },
                        "placement": {
                          "type": "object"
                        },
                        "venue": {
                          "type": "object"
                        }
                      },
                      "required": [
                        "device_id"
                      ],
                      "type": "object"
                    },
                    "maxItems": 100,
                    "type": "array"
                  }
                },
                "required": [
                  "devices"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "devices": [
                    {
                      "device_id": "lobby-nyc-002",
                      "fingerprint": "P_b2c3d4e5f6a7b8c9d0e1f2a3",
                      "location_capability": {
                        "position": "precise",
                        "rails": {
                          "geo_targeted_demand": {
                            "location_source": "screen",
                            "reason": null,
                            "status": "ready"
                          },
                          "inventory_feed": {
                            "location_source": "screen",
                            "reason": null,
                            "status": "ready"
                          },
                          "visit_attribution": {
                            "location_source": "screen",
                            "reason": null,
                            "status": "ready"
                          }
                        }
                      },
                      "monetization_status": "limited",
                      "requirements": {
                        "currently_due": [],
                        "eventually_due": [
                          {
                            "code": "display_size_missing",
                            "doc_url": "https://api.trillboards.com/docs/monetization-requirements#display_size_missing",
                            "field": "display",
                            "message": "Display dimensions are missing. Send display.width / display.height (pixels) so creatives are sized correctly.",
                            "reason": "missing_dimensions"
                          }
                        ]
                      },
                      "screen_id": "6624f1a0c3d4e5f6a7b8ca12",
                      "status": "created",
                      "trillboard_id": "6624f1a0c3d4e5f6a7b8c9d1"
                    }
                  ],
                  "errors": [
                    {
                      "device_id": "lobby-nyc-003",
                      "error": "Device \"lobby-nyc-003\" is already registered to another account and reports to Trillboards under this id; it cannot be registered here.",
                      "error_code": "DEVICE_ID_CLAIMED",
                      "status": "failed"
                    }
                  ],
                  "success": true,
                  "summary": {
                    "blocked": 0,
                    "created": 1,
                    "failed": 1,
                    "limited": 1,
                    "ready": 0,
                    "total": 2,
                    "updated": 0
                  }
                },
                "schema": {
                  "properties": {
                    "devices": {
                      "items": {
                        "properties": {
                          "device_id": {
                            "type": "string"
                          },
                          "fingerprint": {
                            "type": "string"
                          },
                          "location_capability": {
                            "$ref": "#/components/schemas/PartnerLocationCapability"
                          },
                          "monetization_status": {
                            "enum": [
                              "ready",
                              "limited",
                              "blocked"
                            ],
                            "type": "string"
                          },
                          "requirements": {
                            "$ref": "#/components/schemas/PartnerMonetizationRequirements"
                          },
                          "screen_id": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "trillboard_id": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "errors": {
                      "items": {
                        "properties": {
                          "device_id": {
                            "type": "string"
                          },
                          "error": {
                            "type": "string"
                          },
                          "error_code": {
                            "description": "Stable code, e.g. DEVICE_ID_CLAIMED (409 — a device that already reports to Trillboards under this id is registered to another account), REGISTRATION_CHECK_UNAVAILABLE (503 — retry), INVALID_COUNTRY (400).",
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "summary": {
                      "properties": {
                        "created": {
                          "type": "integer"
                        },
                        "failed": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "updated": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Batch processed"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Bulk register devices",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/devices/batch\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"devices\":[{\"device_id\":\"<device_id>\"}]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/devices/batch', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"devices\": [\n      {\n        \"device_id\": \"<device_id>\"\n      }\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/devices/batch\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"devices\": [\n            {\n                \"device_id\": \"<device_id>\",\n            },\n        ],\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/devices/batch');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'devices' => [\n            [\n                'device_id' => '<device_id>',\n            ],\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/devices/batch/orientation": {
      "post": {
        "description": "Update `screen_orientation` for multiple devices/screens in one request.\nAccepts device identifiers (ObjectId, fingerprint, or partner_external_id).\n",
        "operationId": "batchUpdateDeviceOrientation",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "device_ids": {
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "type": "array"
                  },
                  "screen_orientation": {
                    "enum": [
                      "portrait",
                      "landscape",
                      "mixed",
                      "unknown"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "device_ids",
                  "screen_orientation"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "matched_devices": 2,
                  "missing_device_ids": [
                    "lobby-nyc-004"
                  ],
                  "success": true,
                  "total_devices": 3,
                  "updated_screens": 2
                },
                "schema": {
                  "properties": {
                    "matched_devices": {
                      "type": "integer"
                    },
                    "missing_device_ids": {
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "total_devices": {
                      "type": "integer"
                    },
                    "updated_screens": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Batch update result"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Batch update screen orientation",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/devices/batch/orientation\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device_ids\":[\"<device_ids>\"],\"screen_orientation\":\"portrait\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/devices/batch/orientation', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"device_ids\": [\n      \"<device_ids>\"\n    ],\n    \"screen_orientation\": \"portrait\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/devices/batch/orientation\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"device_ids\": [\n            \"<device_ids>\",\n        ],\n        \"screen_orientation\": \"portrait\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/devices/batch/orientation');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'device_ids' => [\n            '<device_ids>',\n        ],\n        'screen_orientation' => 'portrait',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/devices/reconcile": {
      "post": {
        "description": "Read-only diff. Post the `external_ids` you believe you have registered\nand get back how they map to the devices Trillboards actually has for\nyou: `matched`, `missing` (you listed, not registered), `orphan`\n(registered, you did not list), `malformed` (registered with an invalid\nexternal id), and `duplicate` (one external id on more than one device),\nplus a `summary`. Self-service replacement for hand-rolled fleet-sync\ntooling. No writes.\n",
        "operationId": "reconcileDevices",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "external_ids": {
                    "description": "Your device external IDs to reconcile.",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 10000,
                    "type": "array"
                  }
                },
                "required": [
                  "external_ids"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "duplicate": [
                    {
                      "device_ids": [
                        "6624f1a0c3d4e5f6a7b8c9d1",
                        "6624f1a0c3d4e5f6a7b8c9d2"
                      ],
                      "external_id": "lobby-nyc-002"
                    }
                  ],
                  "malformed": [],
                  "matched": [
                    {
                      "device_id": "6624f1a0c3d4e5f6a7b8c9d0",
                      "external_id": "lobby-nyc-001",
                      "has_live_screen": true,
                      "screen_status": "approved"
                    }
                  ],
                  "missing": [
                    "lobby-nyc-004"
                  ],
                  "orphan": [
                    {
                      "device_id": "6624f1a0c3d4e5f6a7b8c9d3",
                      "external_id": "lobby-chi-011",
                      "has_live_screen": true
                    }
                  ],
                  "success": true,
                  "summary": {
                    "duplicate": 1,
                    "malformed": 0,
                    "matched": 1,
                    "missing": 1,
                    "orphan": 1,
                    "registered_active": 3,
                    "requested": 3
                  },
                  "truncated": {
                    "duplicate": false,
                    "malformed": false,
                    "matched": false,
                    "missing": false,
                    "orphan": false
                  }
                },
                "schema": {
                  "properties": {
                    "duplicate": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "malformed": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "matched": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "missing": {
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    },
                    "orphan": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "summary": {
                      "properties": {
                        "duplicate": {
                          "type": "integer"
                        },
                        "malformed": {
                          "type": "integer"
                        },
                        "matched": {
                          "type": "integer"
                        },
                        "missing": {
                          "type": "integer"
                        },
                        "orphan": {
                          "type": "integer"
                        },
                        "registered_active": {
                          "type": "integer"
                        },
                        "requested": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "truncated": {
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Reconciliation diff"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "external_ids missing, empty, or over the limit"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reconcile device fleet",
        "tags": [
          "Devices"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/devices/reconcile\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"external_ids\":[\"<external_ids>\"]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/devices/reconcile', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"external_ids\": [\n      \"<external_ids>\"\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/devices/reconcile\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"external_ids\": [\n            \"<external_ids>\",\n        ],\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/devices/reconcile');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'external_ids' => [\n            '<external_ids>',\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/docs": {
      "get": {
        "description": "Returns an HTML page that renders the partner-API OpenAPI spec via\nRedoc, with a left-nav of every documented endpoint. Intended for\nhumans onboarding to the API; agentic clients should fetch the raw\nYAML at `/docs/openapi/partner-api.yaml` instead.\n",
        "operationId": "partnerDocsBrowser",
        "responses": {
          "200": {
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Redoc-rendered documentation page"
          }
        },
        "security": [],
        "summary": "Browsable HTML documentation index",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "# responds with text/html\ncurl -sS \"https://api.trillboards.com/v1/partner/docs\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/docs');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.text(); // text/html\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/docs\",\n)\nresponse.raise_for_status()\nbody = response.text  # text/html\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/docs');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n// $response is text/html — do not json_decode it.\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/earnings": {
      "get": {
        "description": "Get comprehensive earnings summary including lifetime earnings, current month,\navailable balance, breakdown by source, and payout information.\n",
        "operationId": "getEarnings",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "available_for_payout_cents": 350000,
                    "breakdown": {
                      "direct_campaigns": 3000000,
                      "programmatic_floodgates": 300000,
                      "programmatic_google": 1200000
                    },
                    "next_payout_estimate": {
                      "amount_cents": 350000,
                      "date": "2026-02-01",
                      "stripe_status": "active"
                    },
                    "payout_threshold_cents": 10000,
                    "pending_confirmation_cents": 125000,
                    "revenue_share_percent": 80,
                    "this_month_cents": 450000,
                    "total_lifetime_cents": 4500000
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EarningsResponse"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Earnings summary retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get earnings summary",
        "tags": [
          "Earnings"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/earnings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/earnings', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/earnings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/earnings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/earnings/breakdown": {
      "get": {
        "description": "Get earnings breakdown grouped by day, week, or screen.\nUseful for detailed financial reporting and analysis.\n",
        "operationId": "getEarningsBreakdown",
        "parameters": [
          {
            "description": "Start date (ISO 8601 format)",
            "in": "query",
            "name": "start_date",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "End date (ISO 8601 format)",
            "in": "query",
            "name": "end_date",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "How to group the breakdown",
            "in": "query",
            "name": "group_by",
            "schema": {
              "default": "day",
              "enum": [
                "day",
                "week",
                "screen"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "breakdown": [
                      {
                        "date": "2026-01-25",
                        "earnings_cents": 15000,
                        "impressions": 5000
                      },
                      {
                        "date": "2026-01-24",
                        "earnings_cents": 14200,
                        "impressions": 4733
                      }
                    ],
                    "pending_programmatic": {
                      "confirmation_date": null,
                      "estimated_cents": 0
                    },
                    "period": {
                      "end": "2026-01-26T23:59:59.999Z",
                      "group_by": "day",
                      "start": "2025-12-27T00:00:00.000Z"
                    },
                    "summary": {
                      "direct_cents": 450000,
                      "programmatic_cents": 0,
                      "total_cents": 450000,
                      "total_impressions": 150000
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EarningsBreakdownResponse"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Earnings breakdown retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get detailed earnings breakdown",
        "tags": [
          "Earnings"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/earnings/breakdown\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/earnings/breakdown', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/earnings/breakdown\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/earnings/breakdown');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/earnings/payout": {
      "post": {
        "description": "Request a payout of available earnings to your connected Stripe account.\nRequires Stripe Connect onboarding to be complete.\n\n**Requirements:**\n- Stripe account must be connected and active\n- Available balance must meet minimum threshold ($100 default)\n- If `amount_cents` not specified, full available balance is requested\n",
        "operationId": "requestPayout",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "amount_cents": 350000
              },
              "schema": {
                "properties": {
                  "amount_cents": {
                    "description": "Amount to payout in cents (optional, defaults to available balance)",
                    "minimum": 0,
                    "type": "integer"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "amount_cents": 350000,
                    "estimated_arrival": "2026-01-31",
                    "payout_id": "po_1234567890_abc123",
                    "status": "processing",
                    "stripe_transfer_id": null
                  },
                  "message": "Payout request submitted",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PayoutResponse"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Payout request submitted"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "available_cents": {
                          "type": "integer"
                        },
                        "threshold_cents": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "example": "Minimum payout is $100.00",
                      "type": "string"
                    },
                    "success": {
                      "example": false,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Payout requirements not met"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Request a payout",
        "tags": [
          "Earnings"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/earnings/payout\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"amount_cents\":350000}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/earnings/payout', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"amount_cents\": 350000\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/earnings/payout\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"amount_cents\": 350000,\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/earnings/payout');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'amount_cents' => 350000,\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/earnings/transactions": {
      "get": {
        "description": "Get paginated list of earnings transactions and payouts.\nTransactions are aggregated by day showing impressions and calculated earnings.\n",
        "operationId": "getTransactions",
        "parameters": [
          {
            "description": "Maximum transactions to return",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50,
              "maximum": 100,
              "type": "integer"
            }
          },
          {
            "description": "Offset for pagination",
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "type": "integer"
            }
          },
          {
            "description": "Filter by transaction type",
            "in": "query",
            "name": "type",
            "schema": {
              "default": "all",
              "enum": [
                "all",
                "earning",
                "payout"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "pagination": {
                      "has_more": false,
                      "limit": 50,
                      "offset": 0,
                      "total": 45
                    },
                    "transactions": [
                      {
                        "amount_cents": 15000,
                        "created_at": "2026-01-25T00:00:00.000Z",
                        "date": "2026-01-25",
                        "id": "daily_2026-01-25",
                        "impressions": 5000,
                        "source": "impressions",
                        "type": "earning"
                      },
                      {
                        "amount_cents": 14200,
                        "created_at": "2026-01-24T00:00:00.000Z",
                        "date": "2026-01-24",
                        "id": "daily_2026-01-24",
                        "impressions": 4733,
                        "source": "impressions",
                        "type": "earning"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "pagination": {
                          "$ref": "#/components/schemas/Pagination"
                        },
                        "transactions": {
                          "items": {
                            "$ref": "#/components/schemas/Transaction"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Transactions retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get transaction history",
        "tags": [
          "Earnings"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/earnings/transactions\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/earnings/transactions', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/earnings/transactions\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/earnings/transactions');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/events/connections": {
      "get": {
        "description": "Returns the number of active SSE connections and their details.",
        "operationId": "getActiveSSEConnections",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "connections": [
                    {
                      "connected_at": "2026-08-31T14:22:07.512Z",
                      "connection_id": "conn_mthbumjc_2c4e77b1",
                      "ip": "203.0.113.42",
                      "subscribed_events": [
                        "impression.recorded",
                        "creative.rewards_ready",
                        "device.offline"
                      ]
                    }
                  ],
                  "count": 1,
                  "max_connections": 5,
                  "object": "sse_connections"
                },
                "schema": {
                  "properties": {
                    "connections": {
                      "items": {
                        "properties": {
                          "connected_at": {
                            "format": "date-time",
                            "type": "string"
                          },
                          "connection_id": {
                            "type": "string"
                          },
                          "ip": {
                            "type": "string"
                          },
                          "subscribed_events": {
                            "items": {
                              "type": "string"
                            },
                            "type": "array"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "max_connections": {
                      "example": 5,
                      "type": "integer"
                    },
                    "object": {
                      "example": "sse_connections",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Active connections"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List active SSE connections",
        "tags": [
          "Events"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/events/connections\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/events/connections', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/events/connections\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/events/connections');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/events/stream": {
      "get": {
        "description": "Opens a Server-Sent Events (SSE) connection for real-time event streaming.\nMaximum 5 concurrent connections per partner.\n\n`creative.rewards_ready` carries a free readiness hint for an owned funded experiment.\nIts data uses `CreativeRewardsReadyEvent`: no reward rows are included. Follow the\ncanonical `reward_reader_url` with your own `idempotency_key`; that page uses the\nexisting data_api/api_calls tariff. Rewind the fixed date scope and deduplicate by\ncanonical play key plus reward revision. A hint is not a reward cursor or snapshot.\n\nSupports `Last-Event-ID` header for reconnection and missed event replay.\nThe server sends heartbeat comments (`:`) every 30 seconds to keep the connection alive.\n",
        "operationId": "subscribeToEvents",
        "parameters": [
          {
            "description": "Comma-separated list of event types to subscribe to. Empty subscribes to all events.",
            "example": "device.online,device.offline,impression.recorded",
            "in": "query",
            "name": "events",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "text/event-stream": {
                "schema": {
                  "description": "SSE-formatted messages:\n```\nevent: device.online\nid: evt_1234567890_abc123\ndata: {\"device_id\":\"...\",\"timestamp\":\"...\"}\n```\n",
                  "type": "string"
                }
              }
            },
            "description": "SSE event stream"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Maximum SSE connections reached (5 per partner)"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Subscribe to real-time events (SSE)",
        "tags": [
          "Events"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "# responds with text/event-stream\ncurl -sS \"https://api.trillboards.com/v1/partner/events/stream\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/events/stream', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\n\n// Server-sent events: read the stream, do not await res.json().\nfor await (const chunk of res.body) process.stdout.write(Buffer.from(chunk));\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/events/stream\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    stream=True,\n)\nresponse.raise_for_status()\nfor line in response.iter_lines():\n    print(line.decode())\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/events/stream');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n// $response is text/event-stream — do not json_decode it.\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/fleet-commands/{id}": {
      "get": {
        "description": "Returns a fleet-command job created via POST /screen-groups/{groupId}/commands, including\nper-screen results. Gated by SCREEN_GROUPS_ENABLED.\n",
        "operationId": "getFleetCommand",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "fleet_command": {
                      "command_type": "reload",
                      "completed_at": "2026-08-31T14:05:12.887Z",
                      "created_at": "2026-08-31T14:05:11.402Z",
                      "error": null,
                      "failed": 1,
                      "group_id": "sg_3b8d5c1f-24a7-4e19-8f0b-6d5a92c34e11",
                      "id": "fc_0f3c8e2a-71d4-4b6e-9a55-2c8f4b1d7e60",
                      "object": "fleet_command",
                      "queued": 1,
                      "results": [
                        {
                          "device_command_id": "68b45c0e3f19d27a4b8e10c3",
                          "error": null,
                          "screen_id": "6a1f2c4d9e8b7a0c15d3e2f4",
                          "status": "queued"
                        },
                        {
                          "device_command_id": null,
                          "error": "No device linked",
                          "screen_id": "7b2e3d5a0f9c8b1d26e4f305",
                          "status": "failed"
                        }
                      ],
                      "status": "completed",
                      "targeted": 2,
                      "updated_at": "2026-08-31T14:05:12.887Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "fleet_command": {
                          "$ref": "#/components/schemas/FleetCommand"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The fleet-command job"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Poll a fleet-command job",
        "tags": [
          "Fleet Management"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/fleet-commands/<id>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/fleet-commands/<id>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/fleet-commands/<id>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/fleet-commands/<id>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/fleet/analytics": {
      "get": {
        "description": "Returns fleet-wide performance metrics for the last 24 hours including\ntotal impressions, fill rate, average CPM, and attention score.\n",
        "operationId": "getFleetAnalytics",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "avg_attention": 0.612,
                    "avg_attention_outcome": {
                      "kind": "ok"
                    },
                    "avg_cpm": 4.82,
                    "fill_rate_pct": 62.35,
                    "impressions_24h": 168420,
                    "screen_count": 412
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "avg_attention": {
                          "description": "Average attention score (0-1) over the fleet's audience\nwindows in the last 24 hours. Omitted when it was not\nmeasured — no screen in scope had an audience window, or\nthe audience read failed (see `avg_attention_outcome`);\nnever a `0` that stands in for a missing measurement.\n",
                          "type": "number"
                        },
                        "avg_attention_outcome": {
                          "description": "Whether the attention average above was measured.\n`kind: ok` — the read ran (the average is omitted only when\nno screen had an audience window); `kind: unavailable` —\nthe audience read failed, so the average is omitted.\nIndependent of the other fields, which are served either way.\n",
                          "properties": {
                            "kind": {
                              "enum": [
                                "ok",
                                "unavailable"
                              ],
                              "type": "string"
                            },
                            "message": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "avg_cpm": {
                          "description": "Average CPM in dollars",
                          "type": "number"
                        },
                        "fill_rate_pct": {
                          "description": "Fill rate percentage (0-100)",
                          "type": "number"
                        },
                        "impressions_24h": {
                          "type": "integer"
                        },
                        "screen_count": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Fleet analytics"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get fleet analytics",
        "tags": [
          "Fleet Management"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/fleet/analytics\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/fleet/analytics', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/fleet/analytics\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/fleet/analytics');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/fleet/command": {
      "post": {
        "description": "Sends a command to all devices in the fleet (or a filtered subset).\nCommands expire after 10 minutes. Maximum 500 devices per batch.\n",
        "operationId": "pushFleetCommand",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "command": {
                    "properties": {
                      "payload": {
                        "description": "Command-specific data",
                        "type": "object"
                      },
                      "type": {
                        "enum": [
                          "reload",
                          "update_settings",
                          "restart",
                          "screenshot",
                          "clear_cache",
                          "update_content",
                          "refresh_ads",
                          "update_config",
                          "get_state",
                          "show_ad",
                          "play_ad"
                        ],
                        "type": "string"
                      }
                    },
                    "required": [
                      "type"
                    ],
                    "type": "object"
                  },
                  "filter": {
                    "description": "Optional filter to target specific devices",
                    "properties": {
                      "country": {
                        "type": "string"
                      },
                      "group": {
                        "description": "A screen group id (sg_) whose members are targeted (unioned with screenIds).",
                        "type": "string"
                      },
                      "screenIds": {
                        "items": {
                          "type": "string"
                        },
                        "type": "array"
                      },
                      "venueType": {
                        "type": "string"
                      }
                    },
                    "type": "object"
                  }
                },
                "required": [
                  "command"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "errors": [
                      {
                        "error": "No device linked",
                        "screenId": "7b2e3d5a0f9c8b1d26e4f305"
                      }
                    ],
                    "queued": 117,
                    "targeted": 118
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "errors": {
                          "items": {
                            "properties": {
                              "error": {
                                "type": "string"
                              },
                              "screenId": {
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "queued": {
                          "description": "Devices successfully queued",
                          "type": "integer"
                        },
                        "targeted": {
                          "description": "Total devices matching filter",
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Command queued"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing command.type"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Push command to fleet",
        "tags": [
          "Fleet Management"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/fleet/command\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"command\":{\"type\":\"reload\"}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/fleet/command', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"command\": {\n      \"type\": \"reload\"\n    }\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/fleet/command\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"command\": {\n            \"type\": \"reload\",\n        },\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/fleet/command');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'command' => [\n            'type' => 'reload',\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/fleet/overview": {
      "get": {
        "description": "Returns a summary of all devices in the partner's fleet including\nonline/offline counts, venue type distribution, and country breakdown.\n",
        "operationId": "getFleetOverview",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "countries": {
                      "CA": 41,
                      "US": 371
                    },
                    "offline": 26,
                    "online": 386,
                    "programmatic_enabled": 401,
                    "total_devices": 412,
                    "venue_types": {
                      "entertainment": 41,
                      "retail": 245,
                      "transit": 118,
                      "unknown": 8
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "countries": {
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "example": {
                            "CA": 10,
                            "US": 50
                          },
                          "type": "object"
                        },
                        "offline": {
                          "type": "integer"
                        },
                        "online": {
                          "type": "integer"
                        },
                        "programmatic_enabled": {
                          "type": "integer"
                        },
                        "total_devices": {
                          "type": "integer"
                        },
                        "venue_types": {
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "example": {
                            "office": 8,
                            "retail": 45,
                            "transit": 12
                          },
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Fleet overview"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get fleet overview",
        "tags": [
          "Fleet Management"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/fleet/overview\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/fleet/overview', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/fleet/overview\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/fleet/overview');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/fleet/settings": {
      "post": {
        "description": "Updates settings across all devices in the fleet (or a filtered subset).\nSettings are merged with existing values, not replaced.\n\n**Allowed settings:** `header_bidding_settings`, `content_preferences`, `display_settings`\n",
        "operationId": "updateFleetSettings",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "filter": {
                    "properties": {
                      "country": {
                        "type": "string"
                      },
                      "group": {
                        "description": "A screen group id (sg_) whose members are targeted (unioned with screenIds).",
                        "type": "string"
                      },
                      "screenIds": {
                        "items": {
                          "type": "string"
                        },
                        "type": "array"
                      },
                      "venueType": {
                        "type": "string"
                      }
                    },
                    "type": "object"
                  },
                  "settings": {
                    "description": "Settings to update (merged with existing)",
                    "properties": {
                      "content_preferences": {
                        "type": "object"
                      },
                      "display_settings": {
                        "type": "object"
                      },
                      "header_bidding_settings": {
                        "properties": {
                          "allocation": {
                            "type": "number"
                          },
                          "enabled": {
                            "type": "boolean"
                          },
                          "force_request": {
                            "type": "boolean"
                          },
                          "min_interval_seconds": {
                            "type": "integer"
                          }
                        },
                        "type": "object"
                      }
                    },
                    "type": "object"
                  }
                },
                "required": [
                  "settings"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "errors": [],
                    "targeted": 118,
                    "updated": 118
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "errors": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "targeted": {
                          "type": "integer"
                        },
                        "updated": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Settings updated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Empty settings or invalid keys"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update fleet-wide settings",
        "tags": [
          "Fleet Management"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/fleet/settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"settings\":{}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/fleet/settings', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"settings\": {}\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/fleet/settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"settings\": {},\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/fleet/settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'settings' => [],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/impression": {
      "get": {
        "description": "Record a single impression. Can be called as a tracking pixel (1x1 image load).\n\nBoth GET and POST methods are supported for flexibility.\nDuplicate `impid` + `adid` submissions are ignored (idempotent).\n\n**Optional Signature Verification**\nProvide signature headers to verify impressions:\n```\nX-Trillboards-API-Key: trb_partner_xxxxx\nX-Trillboards-Timestamp: <unix_seconds>\nX-Trillboards-Signature: sha256=<hmac>\n```\nSignature payload:\n```\n{timestamp}.{adid}.{impid}.{did}.{sid}.{aid}\n```\nSignature = HMAC-SHA256(api_key, payload)\n",
        "operationId": "recordImpressionGet",
        "parameters": [
          {
            "description": "Partner API key for optional signature verification",
            "in": "header",
            "name": "X-Trillboards-API-Key",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Unix timestamp for signature verification",
            "in": "header",
            "name": "X-Trillboards-Timestamp",
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "HMAC signature for optional verification",
            "in": "header",
            "name": "X-Trillboards-Signature",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Advertisement ID",
            "in": "query",
            "name": "adid",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Unique impression ID",
            "in": "query",
            "name": "impid",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Device ID, fingerprint, or external device ID",
            "in": "query",
            "name": "did",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Screen ID (optional, for analytics linkage)",
            "in": "query",
            "name": "sid",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Allocation ID",
            "in": "query",
            "name": "aid",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "View duration in seconds",
            "in": "query",
            "name": "duration",
            "schema": {
              "type": "integer"
            }
          },
          {
            "deprecated": true,
            "description": "**Accepted and ignored — this flag does not change what the server records.**\n\nEvery call to this endpoint from a partner-authenticated device records\nexactly one completion event, with or without `completed=true`. That makes\n`tracking.complete_url` and `tracking.impression_url` (which differ only by\nthis parameter) behaviourally identical: **fire exactly one of them per\nplay**, or the play is recorded twice.\n\nThis endpoint is delivery accounting, not proof of play. Billing-grade,\nserver-verified completion is `tracking.owned_direct_complete_url` on the\nad object, which is signed at serve time and cannot be forged or replayed.\n",
            "in": "query",
            "name": "completed",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "Display mode context (fullscreen, l-bar, pip)",
            "in": "query",
            "name": "display_mode",
            "schema": {
              "enum": [
                "fullscreen",
                "l-bar",
                "pip"
              ],
              "type": "string"
            }
          },
          {
            "description": "Screen area ratio used by ad (0.0 to 1.0)",
            "in": "query",
            "name": "display_area_ratio",
            "schema": {
              "type": "number"
            }
          },
          {
            "description": "L-bar size percentage (0-100)",
            "in": "query",
            "name": "lbar_size",
            "schema": {
              "type": "number"
            }
          },
          {
            "description": "L-bar position",
            "in": "query",
            "name": "lbar_position",
            "schema": {
              "enum": [
                "bottom",
                "right"
              ],
              "type": "string"
            }
          },
          {
            "description": "L-bar slot index (0-based)",
            "in": "query",
            "name": "lbar_slot_index",
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Total L-bar slots in rotation",
            "in": "query",
            "name": "lbar_slot_count",
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "HMAC signature (optional, for signed impressions)",
            "in": "query",
            "name": "sig",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Signature timestamp in unix seconds",
            "in": "query",
            "name": "sig_ts",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "message": "Impression recorded",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "duplicate": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Impression recorded"
          }
        },
        "security": [],
        "summary": "Record impression (pixel)",
        "tags": [
          "Impressions"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/impression?adid=%3Cadid%3E&impid=%3Cimpid%3E\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/impression?adid=%3Cadid%3E&impid=%3Cimpid%3E');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/impression?adid=%3Cadid%3E&impid=%3Cimpid%3E\",\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/impression?adid=%3Cadid%3E&impid=%3Cimpid%3E');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      },
      "post": {
        "operationId": "recordImpressionPost",
        "parameters": [
          {
            "description": "Partner API key for optional signature verification",
            "in": "header",
            "name": "X-Trillboards-API-Key",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Unix timestamp for signature verification",
            "in": "header",
            "name": "X-Trillboards-Timestamp",
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "HMAC signature for optional verification",
            "in": "header",
            "name": "X-Trillboards-Signature",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "adid": {
                    "type": "string"
                  },
                  "aid": {
                    "type": "string"
                  },
                  "completed": {
                    "type": "boolean"
                  },
                  "did": {
                    "type": "string"
                  },
                  "display_area_ratio": {
                    "type": "number"
                  },
                  "display_mode": {
                    "enum": [
                      "fullscreen",
                      "l-bar",
                      "pip"
                    ],
                    "type": "string"
                  },
                  "duration": {
                    "type": "integer"
                  },
                  "impid": {
                    "type": "string"
                  },
                  "lbar_position": {
                    "enum": [
                      "bottom",
                      "right"
                    ],
                    "type": "string"
                  },
                  "lbar_size": {
                    "type": "number"
                  },
                  "lbar_slot_count": {
                    "type": "integer"
                  },
                  "lbar_slot_index": {
                    "type": "integer"
                  },
                  "sid": {
                    "type": "string"
                  },
                  "sig": {
                    "type": "string"
                  },
                  "sig_ts": {
                    "type": "integer"
                  }
                },
                "required": [
                  "adid",
                  "impid"
                ],
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "message": "Impression recorded",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "duplicate": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Impression recorded"
          }
        },
        "security": [],
        "summary": "Record impression (POST)",
        "tags": [
          "Impressions"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/impression\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"adid\":\"<adid>\",\"impid\":\"<impid>\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/impression', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"adid\": \"<adid>\",\n    \"impid\": \"<impid>\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/impression\",\n    json={\n        \"adid\": \"<adid>\",\n        \"impid\": \"<impid>\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/impression');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'adid' => '<adid>',\n        'impid' => '<impid>',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/impressions/batch": {
      "post": {
        "description": "Record multiple impressions at once. Useful for syncing offline impressions.\n\nMaximum 100 impressions per request.\nSigned impressions can include `sig` and `sig_ts` per impression (see `/impression` signature format).\n",
        "operationId": "recordImpressionsBatch",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "impressions": {
                    "items": {
                      "properties": {
                        "adid": {
                          "type": "string"
                        },
                        "aid": {
                          "type": "string"
                        },
                        "completed": {
                          "type": "boolean"
                        },
                        "device_fingerprint": {
                          "type": "string"
                        },
                        "did": {
                          "type": "string"
                        },
                        "display_area_ratio": {
                          "type": "number"
                        },
                        "display_mode": {
                          "enum": [
                            "fullscreen",
                            "l-bar",
                            "pip"
                          ],
                          "type": "string"
                        },
                        "duration": {
                          "type": "integer"
                        },
                        "impid": {
                          "type": "string"
                        },
                        "lbar_position": {
                          "enum": [
                            "bottom",
                            "right"
                          ],
                          "type": "string"
                        },
                        "lbar_size": {
                          "type": "number"
                        },
                        "lbar_slot_count": {
                          "type": "integer"
                        },
                        "lbar_slot_index": {
                          "type": "integer"
                        },
                        "sid": {
                          "type": "string"
                        },
                        "sig": {
                          "type": "string"
                        },
                        "sig_ts": {
                          "type": "integer"
                        },
                        "timestamp": {
                          "format": "date-time",
                          "type": "string"
                        }
                      },
                      "required": [
                        "adid",
                        "impid"
                      ],
                      "type": "object"
                    },
                    "maxItems": 100,
                    "type": "array"
                  }
                },
                "required": [
                  "impressions"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "errors": [
                      {
                        "error": "Device not found for signed impression",
                        "impid": "imp_1766644800000_abcd1234"
                      }
                    ],
                    "failed": 1,
                    "recorded": 2,
                    "skipped": 0
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "errors": {
                          "items": {
                            "properties": {
                              "error": {
                                "type": "string"
                              },
                              "impid": {
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "failed": {
                          "type": "integer"
                        },
                        "recorded": {
                          "type": "integer"
                        },
                        "skipped": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Batch processed"
          }
        },
        "security": [],
        "summary": "Batch record impressions",
        "tags": [
          "Impressions"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/impressions/batch\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"impressions\":[{\"adid\":\"<adid>\",\"impid\":\"<impid>\"}]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/impressions/batch', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"impressions\": [\n      {\n        \"adid\": \"<adid>\",\n        \"impid\": \"<impid>\"\n      }\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/impressions/batch\",\n    json={\n        \"impressions\": [\n            {\n                \"adid\": \"<adid>\",\n                \"impid\": \"<impid>\",\n            },\n        ],\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/impressions/batch');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'impressions' => [\n            [\n                'adid' => '<adid>',\n                'impid' => '<impid>',\n            ],\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/info": {
      "get": {
        "description": "Retrieve information about the authenticated partner",
        "operationId": "getPartnerInfo",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "integration_tier": "lite_sdk",
                    "name": "Northwind Retail",
                    "partner_id": "660000000000000000000001",
                    "partner_type": "digital_signage",
                    "revenue_share_percent": 60,
                    "sdk_config": {
                      "ad_interval": 60,
                      "allowed_ad_types": [
                        "video",
                        "image"
                      ],
                      "cache_size": 10,
                      "image_duration": 8,
                      "max_ads_per_hour": 20,
                      "overlay_button_text": "TAP TO CONTINUE",
                      "sound_enabled": false
                    },
                    "slug": "northwind-retail",
                    "stats": {
                      "active_devices": 9,
                      "last_impression_at": "2026-08-25T17:42:11.503Z",
                      "pending_payout_cents": 18450,
                      "total_devices": 12,
                      "total_impressions": 184320,
                      "total_revenue_cents": 246800
                    },
                    "status": "active",
                    "stripe_account_status": "pending"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Partner"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Partner info retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get partner info",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/info\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/info', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/info\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/info');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/intent/categories": {
      "get": {
        "description": "Returns the catalog of purchase intent categories with CPM multipliers\nand matching thresholds.\n",
        "operationId": "listIntentCategories",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "count": 6,
                  "data": [
                    {
                      "description": "Audience actively expressing desire to buy",
                      "id": "active_purchase_intent",
                      "min_intent_score": 0.8,
                      "name": "Active Purchase Intent",
                      "premium_multiplier": 5,
                      "purchase_stages": [
                        "READY_TO_BUY"
                      ]
                    },
                    {
                      "description": "Audience actively comparing prices",
                      "id": "price_comparison",
                      "min_intent_score": 0.55,
                      "name": "Price Comparison",
                      "premium_multiplier": 3.5,
                      "purchase_stages": [
                        "COMPARING"
                      ]
                    },
                    {
                      "description": "Audience showing strong emotional and physical engagement",
                      "id": "high_engagement",
                      "min_intent_score": 0.3,
                      "name": "High Engagement",
                      "premium_multiplier": 1.8,
                      "purchase_stages": [
                        "BROWSING",
                        "CONSIDERING",
                        "COMPARING"
                      ]
                    }
                  ],
                  "object": "list"
                },
                "schema": {
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "data": {
                      "items": {
                        "properties": {
                          "description": {
                            "type": "string"
                          },
                          "id": {
                            "example": "active_purchase_intent",
                            "type": "string"
                          },
                          "min_intent_score": {
                            "description": "Minimum intent score to match this category",
                            "example": 0.8,
                            "type": "number"
                          },
                          "name": {
                            "example": "Active Purchase Intent",
                            "type": "string"
                          },
                          "premium_multiplier": {
                            "description": "CPM multiplier applied when this intent is detected",
                            "example": 5,
                            "type": "number"
                          },
                          "purchase_stages": {
                            "items": {
                              "enum": [
                                "READY_TO_BUY",
                                "COMPARING",
                                "CONSIDERING",
                                "BROWSING"
                              ],
                              "type": "string"
                            },
                            "type": "array"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "object": {
                      "example": "list",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Intent categories"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List intent categories",
        "tags": [
          "Intent Catalog"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/intent/categories\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/intent/categories', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/intent/categories\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/intent/categories');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/intent/categories/{categoryId}": {
      "get": {
        "description": "Returns detailed information about a specific intent category including triggers.",
        "operationId": "getIntentCategory",
        "parameters": [
          {
            "example": "active_purchase_intent",
            "in": "path",
            "name": "categoryId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "description": "Audience actively expressing desire to buy",
                  "iab_segments": [],
                  "id": "active_purchase_intent",
                  "min_intent_score": 0.8,
                  "name": "Active Purchase Intent",
                  "object": "intent_category",
                  "premium_multiplier": 5,
                  "purchase_stages": [
                    "READY_TO_BUY"
                  ],
                  "triggers": [
                    "verbal: \"I want to buy...\", \"How much is...\"",
                    "visual: wallet/card in hand"
                  ]
                },
                "schema": {
                  "properties": {
                    "description": {
                      "type": "string"
                    },
                    "iab_segments": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "id": {
                      "type": "string"
                    },
                    "min_intent_score": {
                      "type": "number"
                    },
                    "name": {
                      "type": "string"
                    },
                    "object": {
                      "example": "intent_category",
                      "type": "string"
                    },
                    "premium_multiplier": {
                      "type": "number"
                    },
                    "purchase_stages": {
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    },
                    "triggers": {
                      "items": {
                        "type": "string"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Intent category details"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Intent category not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get intent category details",
        "tags": [
          "Intent Catalog"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/intent/categories/active_purchase_intent\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/intent/categories/active_purchase_intent', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/intent/categories/active_purchase_intent\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/intent/categories/active_purchase_intent');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/intent/pricing/{screenId}": {
      "get": {
        "description": "Returns real-time intent-based pricing for a specific screen based on\ncurrent audience signals (speech, emotion, behavior). Includes the detected\nintent category, purchase stage, and premium CPM.\n",
        "operationId": "getIntentPricing",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "category_match": null,
                  "current_intent": {
                    "categories": [],
                    "purchase_stage": "NONE",
                    "score": 0.096,
                    "should_trigger": false
                  },
                  "object": "intent_pricing",
                  "pricing": {
                    "base_cpm": 4,
                    "intent_cpm": 4,
                    "intent_premium": 0,
                    "premium_multiplier": 1
                  },
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11"
                },
                "schema": {
                  "properties": {
                    "category_match": {
                      "nullable": true,
                      "properties": {
                        "category_id": {
                          "type": "string"
                        },
                        "category_name": {
                          "type": "string"
                        },
                        "intent_score": {
                          "type": "number"
                        },
                        "premium_multiplier": {
                          "type": "number"
                        },
                        "purchase_stage": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "current_intent": {
                      "properties": {
                        "categories": {
                          "items": {
                            "type": "string"
                          },
                          "type": "array"
                        },
                        "purchase_stage": {
                          "enum": [
                            "NONE",
                            "BROWSING",
                            "CONSIDERING",
                            "COMPARING",
                            "READY_TO_BUY"
                          ],
                          "type": "string"
                        },
                        "score": {
                          "description": "Intent confidence score (0-1)",
                          "type": "number"
                        },
                        "should_trigger": {
                          "description": "Whether an ad swap should be triggered",
                          "type": "boolean"
                        }
                      },
                      "type": "object"
                    },
                    "object": {
                      "example": "intent_pricing",
                      "type": "string"
                    },
                    "pricing": {
                      "properties": {
                        "base_cpm": {
                          "type": "number"
                        },
                        "category_id": {
                          "type": "string"
                        },
                        "category_name": {
                          "type": "string"
                        },
                        "intent_cpm": {
                          "description": "Effective CPM after intent premium",
                          "type": "number"
                        },
                        "intent_premium": {
                          "description": "Additional CPM from intent detection",
                          "type": "number"
                        },
                        "premium_multiplier": {
                          "type": "number"
                        }
                      },
                      "type": "object"
                    },
                    "screen_id": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Intent pricing data"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Invalid screen ID"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Screen not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get intent-based pricing for a screen",
        "tags": [
          "Intent Catalog"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/intent/pricing/<screenId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/intent/pricing/<screenId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/intent/pricing/<screenId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/intent/pricing/<screenId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/measurement-methodology": {
      "get": {
        "description": "Returns the versioned document describing how Trillboards captures\nout-of-home audience signal, resolves device identity, qualifies an\nexposure, and gates every published count. Written for buyer and\nprocurement due-diligence review; built against MRC OOH Phase 2.\n\nPublic — no authentication. Content-negotiated: an `Accept` header\npreferring `text/html` returns a rendered page, anything else returns\nthis JSON. Send `Accept: application/json`, or use\n`/measurement-methodology.json`, to pin the JSON from a browser.\n\n`version` changes whenever a disclosure changes and drives the `ETag`,\nso it is the canonical change-detector for polling clients.\n",
        "operationId": "getMeasurementMethodology",
        "parameters": [
          {
            "description": "Force the JSON representation regardless of `Accept`.",
            "in": "query",
            "name": "format",
            "required": false,
            "schema": {
              "enum": [
                "json"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "canonical_path": "edge capture → buffered ingest into the observation store → signal_observations → 30-minute device-resolution pass → resolved_device_observations → 30-minute transition pass → cross_screen_transitions",
                  "capture_stack": {
                    "ble": "BLE manufacturer-specific data + service UUIDs, with Apple Continuity sub-types extracted. The BLE MAC is stored as received and used for vendor (OUI) lookup.",
                    "gateway_ip": "Gateway IP from agent network telemetry, stored as a keyed hash.",
                    "mdns": "The mDNS service instance name a device advertises, stored as received, plus TXT records against a fixed allowlist — model, vendor, software_version.",
                    "wifi": "WiFi BSSID from network scans, stored as received, plus router-vendor OUI lookup."
                  },
                  "cross_day_matching": {
                    "activated_at": "2026-05-07T00:00:00.000Z",
                    "coverage_disclosure": "Reliable cross-day identity comes from ONE term of the canonical key — stable_payload_hash — and only where that payload is device-distinguishing. A stable payload is present on 24.0% of identifiable BLE rows, but 93.4% of those carry the Apple Continuity \"Nearby Info\" status broadcast (company id 76, sub-type 16), a low-entropy status word shared across whole crowds rather than a per-device value. It is nulled before the coalesce, and those rows fall through to the fallback. EFFECTIVE CROSS-DAY DEVICE-IDENTITY COVERAGE IS THEREFORE 1.6% OF IDENTIFIABLE BLE ROWS (measured on four consecutive days, 2026-08-10 to 2026-08-13: 1.654%, 1.637%, 1.586%, 1.579%). A previous version of this document published ~31%; that figure graded the PRESENCE of stable_payload_hash and did not account for the sanitizer the identity expression applies, and it is withdrawn. The raw_signal_hash fallback is present on ~100% of identifiable rows but is a per-signal HMAC over an address that rotates about every 15 minutes, so it does NOT provide cross-day identity. Randomized-address devices without a device-distinguishing stable payload carry no cross-day device key of their own; where the camera resolves the person seen with one, the person identifier links its device keys across days (evidence_grade_ladder.person_resolution).",
                    "enabled": true,
                    "partner_compatibility": "Partner audience feeds carry a per-partner pseudonymous ID: tb_ followed by the first 16 hex characters of HMAC-SHA256 of the canonical device key, keyed with that partner's own salt (radio_presence.device_level_delivery). One device key carries the same ID in every delivered file to one partner and a DIFFERENT ID for each other partner. The audience loyalty export also carries each device's raw radio identifiers (BLE address, mDNS hostname and WiFi BSSID).",
                    "primary_join_key": "coalesce(if(manufacturer_company_id = 76 AND manufacturer_subtype = 16, NULL, stable_payload_hash), raw_signal_hash)",
                    "soak_disclosure": "Cross-day matching operates only on rows ingested after the 2026-05-07 cutover. Lookback windows spanning that boundary show artificially low cross-day match rates until the rolling window fills with post-cutover rows.",
                    "soak_window_days": 14
                  },
                  "display_exposure_zone": {
                    "calibration_method": "Static per screen-class today. Per-screen RSSI/distance calibration via BLE 5.1 Channel Sounding is planned once the ranging HAL ships on the deployed fleet.",
                    "counting_rule": "Devices outside the contour are not counted as in-range. Devices inside the contour but classified as stationary are excluded from the audience funnel by default, and an operator can include them explicitly.",
                    "definition": "BLE RSSI at or above -75 dBm, per screen — empirically about 5 m line-of-sight for Class-2 BLE radios at typical tablet placement (1.5–2 m mounting).",
                    "rssi_threshold_dbm": -75
                  },
                  "dwell_qualification_thresholds": {
                    "ble_engagement": {
                      "ch_field": "device_session_co_occurrence.bel_ping_count",
                      "definition": "≥5 BLE pings observed across ≥3 distinct minute-buckets within a single visit window",
                      "rationale": "Filters drive-by RSSI spikes; a genuinely dwelling device emits pings continuously rather than as a single burst."
                    },
                    "face_attention": {
                      "ch_field": "device_session_co_occurrence.attention_high",
                      "definition": "attention score above 0.3 sustained for ≥2.5 seconds with face evidence present",
                      "rationale": "MRC OOH Phase 2 viewability surrogate for non-blockable signage; 2.5s is the canonical OAAA OOH attention lower bound."
                    },
                    "returning_visitor": {
                      "ch_field": "distinct_days, derived per canonical visitor over the lookback window",
                      "definition": "≥2 distinct calendar days observed within the lookback window for the same canonical visitor",
                      "rationale": "Filters one-time foot traffic; returning requires real cross-day persistence under the identity layer."
                    }
                  },
                  "edge_confidence_model": {
                    "algorithm": "Bayesian posterior with temporal decay",
                    "decay_window_days": 90,
                    "max_confidence": 0.95
                  },
                  "evidence_grade_ladder": {
                    "csi_backed": {
                      "camera_blind_value": "Restores decisionable evidence in camera-occluded venues, where face and person grades are structurally unavailable.",
                      "decisionable_when": "csi_backed AND correlated with an audio signal in the same window, above the audio signal-quality threshold.",
                      "definition": "WiFi Channel State Information detects at least one occupant at signal quality 0.5 or better — an RF proxy for human presence with no face or person capture.",
                      "hardware": "Dedicated RF sidecar nodes paired to the screen; CSI frames are consumed on the agent.",
                      "rollout_status": "Software-ready since 2026-05-23; fleet coverage is paced by hardware installation."
                    },
                    "decisionable_grades": [
                      "face_backed",
                      "person_backed",
                      "csi_backed"
                    ],
                    "grade_definitions": {
                      "csi_backed": "WiFi Channel State Information presence detection. Counts as decisionable ONLY when the CSI-audio correlation gate fires.",
                      "empty": "Heartbeat frame recorded without an evidence-producing capture window — screen idle, no faces, no audio. Excluded from rate calculations.",
                      "face_backed": "The device holds a tracked face when the window closes (ML Kit face detection and tracking; a track is a face seen in the last 3 seconds), or the window averages 1.0 or more faces per frame; a window that arrives without a device grade is face_backed at a mean of 0.5 faces or more. The grade carries no attention gate: the attention-qualified window is a separate rule (dwell_qualification_thresholds.face_attention). On-device age and gender inference (FaceXFormer) is attached when that processor runs.",
                      "object_backed": "On-device object detection or a local VLM produced structured object or vehicle evidence. Decisionable for object-based sensing and alerts, but excluded from audience demographics and attended-impression measurement.",
                      "person_backed": "Edge person-detection (no face crop) observed body or silhouette evidence — lower confidence than face_backed, still a primary visual signal.",
                      "proxy_only": "BLE, mDNS or WiFi proximity signal only, with no on-device vision or audio evidence. Used for in-range counts, never for attention or attended-impression denominators.",
                      "speech_only": "Audio VAD plus speech detection without corroborating visual evidence; non-decisionable for MRC audience metrics."
                    },
                    "grades": [
                      "face_backed",
                      "person_backed",
                      "object_backed",
                      "csi_backed",
                      "speech_only",
                      "proxy_only",
                      "empty"
                    ],
                    "person_resolution": {
                      "confidence_floor": 0.5,
                      "confidence_gate": "Only crosswalk rows at confidence 0.5 or above contribute a person merge; below the floor a device falls back to its own key. This bounds over-collapse from low-confidence resolutions.",
                      "definition": "Optional PERSON-grain reach and frequency: device-grain counts are collapsed onto a stable person identity so one human is counted once across the several devices they carry. Person-grain is a strict de-duplication of device-grain.",
                      "deletability": "An erasure request deletes the device's rows from signal_observations, resolved_device_observations, cross_screen_transitions and observation_stream. The person identity crosswalk, the encrypted archive and backups keep their rows for the periods in Data Processing Addendum Section 6.",
                      "identity_source": "The person identity crosswalk. The person key is derived from a cluster of face-identity templates (512-number vectors computed on screens running the full sensing profile) and linked to the device key of the device seen with that person, and through it to mobile advertising IDs. It is published as an opaque hash. The templates themselves are excluded from every partner-facing and buyer-facing export.",
                      "opaque": true,
                      "person_count_le_device_count": true,
                      "rail_separation": "The measurement rail and the activation rail are separate reach keys. Trillboards links them in its identity graph, and data valuation partners receive the links between sensed devices and mobile advertising IDs (Data Processing Addendum Section 4.3).",
                      "reach_key_activation": "coalesce(nullIf(person_id, ''), maid_hash) — activation / cohort rail; falls back to the maid hash when a device has no resolved person.",
                      "reach_key_measurement": "coalesce(nullIf(person_id, ''), device_key) — measurement rail; falls back to the canonical device key when a device has no resolved person."
                    }
                  },
                  "fallback_path": "If the observation store is unavailable, observations are buffered on a durable queue and replayed on recovery; after repeated failures a record lands in a dead-letter store rather than being dropped.",
                  "hashing": {
                    "algorithm": "HMAC-SHA256",
                    "cross_day_rejoin": "ENABLED, and effective on a small minority of rows — see coverage_disclosure, which is the number that matters. Cross-day device matching uses the canonical visitor identity coalesce(if(manufacturer_company_id = 76 AND manufacturer_subtype = 16, NULL, stable_payload_hash), raw_signal_hash) — RSSI- and screen-independent, leading with the device-stable payload hash where that payload is device-distinguishing. It deliberately does NOT lead with the BLE MAC: a BLE MAC is a resolvable private address that rotates about every 15 minutes, so leading with it over-counts unique visitors and collapses returning-visitor rates toward zero. The final term of the coalesce IS a per-signal hash of that rotating address, so on any row where the leading term is absent the key inherits exactly that behaviour.",
                    "kek_rotation_frequency_days": 90,
                    "pepper_strategy": "Identifiers are stored as received and also keyed with HMAC-SHA256 under a pepper held in a managed key store; the pepper is encrypted at rest and never written to logs or exports. Right-to-erasure is served by per-device deletion in the active stores (evidence_grade_ladder.person_resolution.deletability)."
                  },
                  "identifier_inflation": {
                    "disclosure": "Every in-range, reach and unique-device count derived from BLE proximity is a count of IDENTIFIERS resolved by the canonical key. Because that key falls back to a per-signal hash of a rotating address on 98.4% of identifiable rows, one physical device contributes several identifiers over a window, and the number grows with window length. These counts must not be read as people, and a longer window is not a proportionally larger audience.",
                    "measured_identifiers_per_device": {
                      "fixed_placement_fleet": {
                        "1_day": 1.44,
                        "4_day": 1.76
                      },
                      "measured_on": "2026-08-10..2026-08-13",
                      "vehicle_mounted_fleet": {
                        "1_day": 3.23,
                        "3_day": 3.97,
                        "7_day": 4.39
                      }
                    },
                    "measurement_design": "Identifiers-per-device is measured against the sub-population where a device-distinguishing stable payload makes the true device observable. Both arms are the same rows over the same window on the same screens; the only variable is whether the row is keyed by its device-distinguishing payload or by the rotating-address fallback the production key actually uses for that population. This is a controlled comparison, not a ratio of two independently-collected counts.",
                    "no_global_correction_factor": "No single divisor is applied, and none is published as one. The factor is fleet-specific (3.23x against 1.44x on the same day, above), grows with window length, and a constant fitted to the middle of that range would be wrong for every fleet at the ends of it. These figures bound the inflation; they are not a correction to apply. The published counts remain identifier counts, and should be compared with other identifier counts rather than with population.",
                    "seen_exactly_once_share": "On one 4-day window, identical rows: 85.6% of TRUE devices were seen on exactly one day, against 98.1% of the identifiers the fallback key produces from those same devices. The \"seen only once\" share of a proximity audience is therefore substantially a property of the KEY, not only of the audience — though it is high under both, because a moving fleet genuinely does meet most people once.",
                    "unit": "Distinct canonical BLE keys in the window."
                  },
                  "mrc_ooh_phase2_alignment": {
                    "audience_qualification_chain": "in_range → face_co_observed → attended → deep_dwell → returning → cross_screen",
                    "ci_methodology": "Wilson score intervals for proportions (in_range_rate, attended_rate and the rest); 200-sample bootstrap percentile intervals for medians (dwell_p50, attention_p50).",
                    "independent_verification": "A partner or MRC reviewer can re-derive every published count by joining the tables listed under source_tables directly. Schema documentation is available on request under NDA.",
                    "methodology_change_log": "Every meaningful disclosure change bumps the version string, which drives the ETag. The version is the canonical change-detector for partner-side scrapers.",
                    "monotonicity_guarantee": "The chain is a strict subset chain, enforced by an invariant test in CI. At request time the endpoint returns HTTP 500 with a diagnostic if a monotonicity violation is detected after the query — defence in depth, so a query defect cannot silently ship impossible numbers to a partner.",
                    "mrc_status_accredited": false,
                    "status": "self-aligned; not accredited. Accreditation is granted by the Media Rating Council following a formal application and an independent CPA audit engagement; no such grant has been issued to Trillboards."
                  },
                  "parser_versions": {
                    "ble_advertisement_parser": "1.0",
                    "heartbeat_payload_schema": 1,
                    "mdns_txt_extractor": "1.0"
                  },
                  "retention": {
                    "basis": "Days each table keeps a row in active systems. Identity nodes and edges count from the last time the device or link was seen. The encrypted archive keeps verbatim radio identifiers, derived device keys, exposure windows and person identifiers with no scheduled deletion.",
                    "cross_screen_transitions_days": 400,
                    "identity_edges_days": 90,
                    "identity_nodes_days": 90,
                    "resolved_device_observations_days": 400,
                    "signal_observations_days": 14
                  },
                  "source_tables": [
                    "signal_observations",
                    "resolved_device_observations",
                    "cross_screen_transitions",
                    "device_session_co_occurrence",
                    "device_stationary_score",
                    "identity_nodes",
                    "identity_edges"
                  ],
                  "version": "2026-08-15"
                },
                "schema": {
                  "$ref": "#/components/schemas/MeasurementMethodology"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Methodology document (JSON) or rendered page (HTML)",
            "headers": {
              "ETag": {
                "description": "`\"methodology-<version>\"` for JSON, `\"methodology-<version>+html\"`\nfor the page, so a shared cache cannot serve one for the other.\n",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "Always `Accept` — the response varies by media type.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [],
        "summary": "Trillboards measurement methodology disclosure",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/measurement-methodology\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/measurement-methodology');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/measurement-methodology\",\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/measurement-methodology');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/measurement-methodology.json": {
      "get": {
        "description": "Identical payload to `GET /measurement-methodology`, with the JSON\nrepresentation pinned. Exists so a browser — whose `Accept` header\nalways prefers `text/html` — can reach the machine-readable document\nwithout setting headers.\n",
        "operationId": "getMeasurementMethodologyJson",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "canonical_path": "edge capture → buffered ingest into the observation store → signal_observations → 30-minute device-resolution pass → resolved_device_observations → 30-minute transition pass → cross_screen_transitions",
                  "capture_stack": {
                    "ble": "BLE manufacturer-specific data + service UUIDs, with Apple Continuity sub-types extracted. The BLE MAC is stored as received and used for vendor (OUI) lookup.",
                    "gateway_ip": "Gateway IP from agent network telemetry, stored as a keyed hash.",
                    "mdns": "The mDNS service instance name a device advertises, stored as received, plus TXT records against a fixed allowlist — model, vendor, software_version.",
                    "wifi": "WiFi BSSID from network scans, stored as received, plus router-vendor OUI lookup."
                  },
                  "cross_day_matching": {
                    "activated_at": "2026-05-07T00:00:00.000Z",
                    "coverage_disclosure": "Reliable cross-day identity comes from ONE term of the canonical key — stable_payload_hash — and only where that payload is device-distinguishing. A stable payload is present on 24.0% of identifiable BLE rows, but 93.4% of those carry the Apple Continuity \"Nearby Info\" status broadcast (company id 76, sub-type 16), a low-entropy status word shared across whole crowds rather than a per-device value. It is nulled before the coalesce, and those rows fall through to the fallback. EFFECTIVE CROSS-DAY DEVICE-IDENTITY COVERAGE IS THEREFORE 1.6% OF IDENTIFIABLE BLE ROWS (measured on four consecutive days, 2026-08-10 to 2026-08-13: 1.654%, 1.637%, 1.586%, 1.579%). A previous version of this document published ~31%; that figure graded the PRESENCE of stable_payload_hash and did not account for the sanitizer the identity expression applies, and it is withdrawn. The raw_signal_hash fallback is present on ~100% of identifiable rows but is a per-signal HMAC over an address that rotates about every 15 minutes, so it does NOT provide cross-day identity. Randomized-address devices without a device-distinguishing stable payload carry no cross-day device key of their own; where the camera resolves the person seen with one, the person identifier links its device keys across days (evidence_grade_ladder.person_resolution).",
                    "enabled": true,
                    "partner_compatibility": "Partner audience feeds carry a per-partner pseudonymous ID: tb_ followed by the first 16 hex characters of HMAC-SHA256 of the canonical device key, keyed with that partner's own salt (radio_presence.device_level_delivery). One device key carries the same ID in every delivered file to one partner and a DIFFERENT ID for each other partner. The audience loyalty export also carries each device's raw radio identifiers (BLE address, mDNS hostname and WiFi BSSID).",
                    "primary_join_key": "coalesce(if(manufacturer_company_id = 76 AND manufacturer_subtype = 16, NULL, stable_payload_hash), raw_signal_hash)",
                    "soak_disclosure": "Cross-day matching operates only on rows ingested after the 2026-05-07 cutover. Lookback windows spanning that boundary show artificially low cross-day match rates until the rolling window fills with post-cutover rows.",
                    "soak_window_days": 14
                  },
                  "display_exposure_zone": {
                    "calibration_method": "Static per screen-class today. Per-screen RSSI/distance calibration via BLE 5.1 Channel Sounding is planned once the ranging HAL ships on the deployed fleet.",
                    "counting_rule": "Devices outside the contour are not counted as in-range. Devices inside the contour but classified as stationary are excluded from the audience funnel by default, and an operator can include them explicitly.",
                    "definition": "BLE RSSI at or above -75 dBm, per screen — empirically about 5 m line-of-sight for Class-2 BLE radios at typical tablet placement (1.5–2 m mounting).",
                    "rssi_threshold_dbm": -75
                  },
                  "dwell_qualification_thresholds": {
                    "ble_engagement": {
                      "ch_field": "device_session_co_occurrence.bel_ping_count",
                      "definition": "≥5 BLE pings observed across ≥3 distinct minute-buckets within a single visit window",
                      "rationale": "Filters drive-by RSSI spikes; a genuinely dwelling device emits pings continuously rather than as a single burst."
                    },
                    "face_attention": {
                      "ch_field": "device_session_co_occurrence.attention_high",
                      "definition": "attention score above 0.3 sustained for ≥2.5 seconds with face evidence present",
                      "rationale": "MRC OOH Phase 2 viewability surrogate for non-blockable signage; 2.5s is the canonical OAAA OOH attention lower bound."
                    },
                    "returning_visitor": {
                      "ch_field": "distinct_days, derived per canonical visitor over the lookback window",
                      "definition": "≥2 distinct calendar days observed within the lookback window for the same canonical visitor",
                      "rationale": "Filters one-time foot traffic; returning requires real cross-day persistence under the identity layer."
                    }
                  },
                  "edge_confidence_model": {
                    "algorithm": "Bayesian posterior with temporal decay",
                    "decay_window_days": 90,
                    "max_confidence": 0.95
                  },
                  "evidence_grade_ladder": {
                    "csi_backed": {
                      "camera_blind_value": "Restores decisionable evidence in camera-occluded venues, where face and person grades are structurally unavailable.",
                      "decisionable_when": "csi_backed AND correlated with an audio signal in the same window, above the audio signal-quality threshold.",
                      "definition": "WiFi Channel State Information detects at least one occupant at signal quality 0.5 or better — an RF proxy for human presence with no face or person capture.",
                      "hardware": "Dedicated RF sidecar nodes paired to the screen; CSI frames are consumed on the agent.",
                      "rollout_status": "Software-ready since 2026-05-23; fleet coverage is paced by hardware installation."
                    },
                    "decisionable_grades": [
                      "face_backed",
                      "person_backed",
                      "csi_backed"
                    ],
                    "grade_definitions": {
                      "csi_backed": "WiFi Channel State Information presence detection. Counts as decisionable ONLY when the CSI-audio correlation gate fires.",
                      "empty": "Heartbeat frame recorded without an evidence-producing capture window — screen idle, no faces, no audio. Excluded from rate calculations.",
                      "face_backed": "The device holds a tracked face when the window closes (ML Kit face detection and tracking; a track is a face seen in the last 3 seconds), or the window averages 1.0 or more faces per frame; a window that arrives without a device grade is face_backed at a mean of 0.5 faces or more. The grade carries no attention gate: the attention-qualified window is a separate rule (dwell_qualification_thresholds.face_attention). On-device age and gender inference (FaceXFormer) is attached when that processor runs.",
                      "object_backed": "On-device object detection or a local VLM produced structured object or vehicle evidence. Decisionable for object-based sensing and alerts, but excluded from audience demographics and attended-impression measurement.",
                      "person_backed": "Edge person-detection (no face crop) observed body or silhouette evidence — lower confidence than face_backed, still a primary visual signal.",
                      "proxy_only": "BLE, mDNS or WiFi proximity signal only, with no on-device vision or audio evidence. Used for in-range counts, never for attention or attended-impression denominators.",
                      "speech_only": "Audio VAD plus speech detection without corroborating visual evidence; non-decisionable for MRC audience metrics."
                    },
                    "grades": [
                      "face_backed",
                      "person_backed",
                      "object_backed",
                      "csi_backed",
                      "speech_only",
                      "proxy_only",
                      "empty"
                    ],
                    "person_resolution": {
                      "confidence_floor": 0.5,
                      "confidence_gate": "Only crosswalk rows at confidence 0.5 or above contribute a person merge; below the floor a device falls back to its own key. This bounds over-collapse from low-confidence resolutions.",
                      "definition": "Optional PERSON-grain reach and frequency: device-grain counts are collapsed onto a stable person identity so one human is counted once across the several devices they carry. Person-grain is a strict de-duplication of device-grain.",
                      "deletability": "An erasure request deletes the device's rows from signal_observations, resolved_device_observations, cross_screen_transitions and observation_stream. The person identity crosswalk, the encrypted archive and backups keep their rows for the periods in Data Processing Addendum Section 6.",
                      "identity_source": "The person identity crosswalk. The person key is derived from a cluster of face-identity templates (512-number vectors computed on screens running the full sensing profile) and linked to the device key of the device seen with that person, and through it to mobile advertising IDs. It is published as an opaque hash. The templates themselves are excluded from every partner-facing and buyer-facing export.",
                      "opaque": true,
                      "person_count_le_device_count": true,
                      "rail_separation": "The measurement rail and the activation rail are separate reach keys. Trillboards links them in its identity graph, and data valuation partners receive the links between sensed devices and mobile advertising IDs (Data Processing Addendum Section 4.3).",
                      "reach_key_activation": "coalesce(nullIf(person_id, ''), maid_hash) — activation / cohort rail; falls back to the maid hash when a device has no resolved person.",
                      "reach_key_measurement": "coalesce(nullIf(person_id, ''), device_key) — measurement rail; falls back to the canonical device key when a device has no resolved person."
                    }
                  },
                  "fallback_path": "If the observation store is unavailable, observations are buffered on a durable queue and replayed on recovery; after repeated failures a record lands in a dead-letter store rather than being dropped.",
                  "hashing": {
                    "algorithm": "HMAC-SHA256",
                    "cross_day_rejoin": "ENABLED, and effective on a small minority of rows — see coverage_disclosure, which is the number that matters. Cross-day device matching uses the canonical visitor identity coalesce(if(manufacturer_company_id = 76 AND manufacturer_subtype = 16, NULL, stable_payload_hash), raw_signal_hash) — RSSI- and screen-independent, leading with the device-stable payload hash where that payload is device-distinguishing. It deliberately does NOT lead with the BLE MAC: a BLE MAC is a resolvable private address that rotates about every 15 minutes, so leading with it over-counts unique visitors and collapses returning-visitor rates toward zero. The final term of the coalesce IS a per-signal hash of that rotating address, so on any row where the leading term is absent the key inherits exactly that behaviour.",
                    "kek_rotation_frequency_days": 90,
                    "pepper_strategy": "Identifiers are stored as received and also keyed with HMAC-SHA256 under a pepper held in a managed key store; the pepper is encrypted at rest and never written to logs or exports. Right-to-erasure is served by per-device deletion in the active stores (evidence_grade_ladder.person_resolution.deletability)."
                  },
                  "identifier_inflation": {
                    "disclosure": "Every in-range, reach and unique-device count derived from BLE proximity is a count of IDENTIFIERS resolved by the canonical key. Because that key falls back to a per-signal hash of a rotating address on 98.4% of identifiable rows, one physical device contributes several identifiers over a window, and the number grows with window length. These counts must not be read as people, and a longer window is not a proportionally larger audience.",
                    "measured_identifiers_per_device": {
                      "fixed_placement_fleet": {
                        "1_day": 1.44,
                        "4_day": 1.76
                      },
                      "measured_on": "2026-08-10..2026-08-13",
                      "vehicle_mounted_fleet": {
                        "1_day": 3.23,
                        "3_day": 3.97,
                        "7_day": 4.39
                      }
                    },
                    "measurement_design": "Identifiers-per-device is measured against the sub-population where a device-distinguishing stable payload makes the true device observable. Both arms are the same rows over the same window on the same screens; the only variable is whether the row is keyed by its device-distinguishing payload or by the rotating-address fallback the production key actually uses for that population. This is a controlled comparison, not a ratio of two independently-collected counts.",
                    "no_global_correction_factor": "No single divisor is applied, and none is published as one. The factor is fleet-specific (3.23x against 1.44x on the same day, above), grows with window length, and a constant fitted to the middle of that range would be wrong for every fleet at the ends of it. These figures bound the inflation; they are not a correction to apply. The published counts remain identifier counts, and should be compared with other identifier counts rather than with population.",
                    "seen_exactly_once_share": "On one 4-day window, identical rows: 85.6% of TRUE devices were seen on exactly one day, against 98.1% of the identifiers the fallback key produces from those same devices. The \"seen only once\" share of a proximity audience is therefore substantially a property of the KEY, not only of the audience — though it is high under both, because a moving fleet genuinely does meet most people once.",
                    "unit": "Distinct canonical BLE keys in the window."
                  },
                  "mrc_ooh_phase2_alignment": {
                    "audience_qualification_chain": "in_range → face_co_observed → attended → deep_dwell → returning → cross_screen",
                    "ci_methodology": "Wilson score intervals for proportions (in_range_rate, attended_rate and the rest); 200-sample bootstrap percentile intervals for medians (dwell_p50, attention_p50).",
                    "independent_verification": "A partner or MRC reviewer can re-derive every published count by joining the tables listed under source_tables directly. Schema documentation is available on request under NDA.",
                    "methodology_change_log": "Every meaningful disclosure change bumps the version string, which drives the ETag. The version is the canonical change-detector for partner-side scrapers.",
                    "monotonicity_guarantee": "The chain is a strict subset chain, enforced by an invariant test in CI. At request time the endpoint returns HTTP 500 with a diagnostic if a monotonicity violation is detected after the query — defence in depth, so a query defect cannot silently ship impossible numbers to a partner.",
                    "mrc_status_accredited": false,
                    "status": "self-aligned; not accredited. Accreditation is granted by the Media Rating Council following a formal application and an independent CPA audit engagement; no such grant has been issued to Trillboards."
                  },
                  "parser_versions": {
                    "ble_advertisement_parser": "1.0",
                    "heartbeat_payload_schema": 1,
                    "mdns_txt_extractor": "1.0"
                  },
                  "retention": {
                    "basis": "Days each table keeps a row in active systems. Identity nodes and edges count from the last time the device or link was seen. The encrypted archive keeps verbatim radio identifiers, derived device keys, exposure windows and person identifiers with no scheduled deletion.",
                    "cross_screen_transitions_days": 400,
                    "identity_edges_days": 90,
                    "identity_nodes_days": 90,
                    "resolved_device_observations_days": 400,
                    "signal_observations_days": 14
                  },
                  "source_tables": [
                    "signal_observations",
                    "resolved_device_observations",
                    "cross_screen_transitions",
                    "device_session_co_occurrence",
                    "device_stationary_score",
                    "identity_nodes",
                    "identity_edges"
                  ],
                  "version": "2026-08-15"
                },
                "schema": {
                  "$ref": "#/components/schemas/MeasurementMethodology"
                }
              }
            },
            "description": "Methodology document",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [],
        "summary": "Measurement methodology disclosure (JSON, no negotiation)",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/measurement-methodology.json\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/measurement-methodology.json');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/measurement-methodology.json\",\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/measurement-methodology.json');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/network-config": {
      "get": {
        "description": "Returns only partner-safe network controls. Internal demand names and\nseller configuration are never exposed. Runtime ad-delivery changes\napply to the next `/device/{deviceId}/ads` response; a response already\nin progress keeps its original timeout and source order.\n",
        "operationId": "getNetworkConfig",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "network_config": {
                    "ad_delivery": {
                      "cache_max_age_seconds": 30,
                      "max_ads_per_hour": 30,
                      "non_personalized_ads": false,
                      "request_interval_seconds": 120,
                      "self_promo_priority_mode": "fallback_only"
                    },
                    "branding": {
                      "display_name": "Bowling Center TV",
                      "favicon_url": "https://cdn.example.com/northwind/favicon.png",
                      "font_body": "Roboto",
                      "font_heading": "Poppins",
                      "logo_url": "https://cdn.example.com/northwind/logo.svg",
                      "primary_color": "210 90% 40%",
                      "secondary_color": "211 100% 99%",
                      "tagline": "Reach every lane in America"
                    },
                    "default_content_preferences": {
                      "allow_alcohol": false,
                      "allow_gambling": false,
                      "blocked_categories": [
                        "politics"
                      ],
                      "content_filtering_enabled": true,
                      "max_age_rating": "PG-13",
                      "preference_level": "moderate"
                    },
                    "default_waterfall": {
                      "source_order": [
                        "var_e4f66f",
                        "var_fa068b"
                      ],
                      "total_timeout_ms": 15000
                    },
                    "storefront": {
                      "enabled": true
                    }
                  },
                  "object": "network_config",
                  "parent_partner_id": null
                },
                "schema": {
                  "properties": {
                    "network_config": {
                      "$ref": "#/components/schemas/PartnerNetworkConfig"
                    },
                    "object": {
                      "example": "network_config",
                      "type": "string"
                    },
                    "parent_partner_id": {
                      "nullable": true,
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Network configuration"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get network configuration",
        "tags": [
          "Network Config"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/network-config\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/network-config', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/network-config\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/network-config');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Partial update of specific fields within the network configuration.\nOnly the provided nested keys are updated; unmentioned keys are preserved.\n\n**Accepted top-level keys:** `default_content_preferences`, `default_waterfall`, `branding`, `storefront`, `ad_delivery`\n\nSource order, timeout, polling guidance, and cache changes apply on the\nnext `/ads` request. Clients should snapshot `total_timeout_ms` and the\nordered `sources` array for the current ad break instead of changing a\nloading deadline midway through playback.\n",
        "operationId": "patchNetworkConfig",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerNetworkConfigUpdate"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "network_config": {
                    "ad_delivery": {
                      "cache_max_age_seconds": 60,
                      "max_ads_per_hour": 30,
                      "non_personalized_ads": true,
                      "request_interval_seconds": 120,
                      "self_promo_priority_mode": "disabled"
                    },
                    "default_waterfall": {
                      "source_order": [
                        "var_e4f66f",
                        "var_fa068b"
                      ],
                      "total_timeout_ms": 15000
                    },
                    "storefront": {
                      "enabled": false
                    }
                  },
                  "object": "network_config",
                  "parent_partner_id": "5f9d1a3c7e2b48d016c4a9f2"
                },
                "schema": {
                  "properties": {
                    "network_config": {
                      "$ref": "#/components/schemas/PartnerNetworkConfig"
                    },
                    "object": {
                      "example": "network_config",
                      "type": "string"
                    },
                    "parent_partner_id": {
                      "nullable": true,
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Configuration updated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "No valid fields to update"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "example": "white_label_distributor_not_linked",
                          "type": "string"
                        },
                        "param": {
                          "example": "branding",
                          "type": "string"
                        },
                        "type": {
                          "example": "invalid_request_error",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "example": false,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "`branding` was supplied but no white-label buyer portal is linked to this\npartner, so the values would not render anywhere. Nothing was saved — including\nthe other sections of the same request. Ask Trillboards to link one.\n"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Partial update network configuration",
        "tags": [
          "Network Config"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/network-config\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"default_content_preferences\":{}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/network-config', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"default_content_preferences\": {}\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/network-config\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"default_content_preferences\": {},\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/network-config');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'default_content_preferences' => [],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "put": {
        "description": "Full update of network configuration. Each provided section is merged\nwith the existing configuration (not replaced entirely).\n\n`default_waterfall.source_order` accepts only opaque source names copied\nfrom an `/ads` response. Unlisted sources remain enabled and are appended\nin their existing order. CPM sorting is intentionally unsupported because\nconfigured CPM is not realized yield. An explicit per-screen waterfall\noverride takes precedence over this partner default.\n",
        "operationId": "updateNetworkConfig",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerNetworkConfigUpdate"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "network_config": {
                    "ad_delivery": {
                      "cache_max_age_seconds": 0,
                      "identity_mode": "ima_sdk",
                      "max_ads_per_hour": 40,
                      "non_personalized_ads": false,
                      "request_interval_seconds": 90,
                      "self_promo_priority_mode": "scheduled_primary"
                    },
                    "default_content_preferences": {
                      "allow_alcohol": false,
                      "allow_gambling": false,
                      "blocked_categories": [
                        "politics"
                      ],
                      "content_filtering_enabled": true,
                      "max_age_rating": "PG-13",
                      "preference_level": "moderate"
                    },
                    "default_waterfall": {
                      "source_order": [
                        "var_e4f66f",
                        "var_fa068b"
                      ],
                      "total_timeout_ms": 12000
                    },
                    "storefront": {
                      "enabled": true
                    }
                  },
                  "object": "network_config",
                  "parent_partner_id": null
                },
                "schema": {
                  "properties": {
                    "network_config": {
                      "$ref": "#/components/schemas/PartnerNetworkConfig"
                    },
                    "object": {
                      "example": "network_config",
                      "type": "string"
                    },
                    "parent_partner_id": {
                      "nullable": true,
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Configuration updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "error": {
                      "properties": {
                        "code": {
                          "example": "white_label_distributor_not_linked",
                          "type": "string"
                        },
                        "param": {
                          "example": "branding",
                          "type": "string"
                        },
                        "type": {
                          "example": "invalid_request_error",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "example": false,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "`branding` was supplied but no white-label buyer portal is linked to this\npartner, so the values would not render anywhere. Nothing was saved — including\nthe other sections of the same request. Ask Trillboards to link one.\n"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update network configuration",
        "tags": [
          "Network Config"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/network-config\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"default_content_preferences\":{}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/network-config', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"default_content_preferences\": {}\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/network-config\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"default_content_preferences\": {},\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/network-config');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'default_content_preferences' => [],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/network/trends": {
      "get": {
        "description": "Returns daily-aggregate audience trends across the Trillboards\nnetwork — venue-type breakdowns, audience composition shifts,\ndevice-density changes — for the partner's licensed regions.\nNetwork-level aggregates; no per-screen rows.\n",
        "operationId": "getNetworkTrends",
        "parameters": [
          {
            "description": "Lookback window",
            "in": "query",
            "name": "days",
            "schema": {
              "default": 30,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "activeScreens": 128,
                  "avgAttention": 0.61,
                  "avgDailyViewers": 4.7,
                  "avgEngagement": 0.58,
                  "dominantMood": "neutral",
                  "growthRate": 0.084,
                  "peakHour": 18,
                  "peakViewers": 41,
                  "period": "7d",
                  "topDemographics": {
                    "incomeHighPct": 0,
                    "incomePremiumPct": 0
                  },
                  "totalMeasurements": 1842500,
                  "typicalOccupancy": "0-5"
                },
                "schema": {
                  "properties": {
                    "audience_shift": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "days": {
                      "type": "integer"
                    },
                    "venue_type_mix": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Trend payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Network-wide audience and inventory trends",
        "tags": [
          "Venue Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/network/trends\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/network/trends', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/network/trends\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/network/trends');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/ping": {
      "get": {
        "description": "Returns a 200 with a tiny JSON payload identifying the partner-API\nservice. Used by load balancers, uptime monitors, and SDK install\nverification. No authentication required.\n",
        "operationId": "partnerPing",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "status": "ok",
                  "timestamp": 1756742400000
                },
                "schema": {
                  "properties": {
                    "ok": {
                      "example": true,
                      "type": "boolean"
                    },
                    "service": {
                      "example": "partner-api",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Liveness payload"
          }
        },
        "security": [],
        "summary": "Unauthenticated liveness probe",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/ping\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/ping');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/ping\",\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/ping');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/ping/auth": {
      "get": {
        "description": "Returns a 200 with a JSON payload including the partner ID derived\nfrom the supplied bearer token. Used during partner onboarding to\nconfirm an API key is valid and to inspect the active tier.\n",
        "operationId": "partnerPingAuth",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "partner_id": "660000000000000000000001",
                  "status": "ok",
                  "timestamp": 1756742400000
                },
                "schema": {
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "partner_id": {
                      "type": "string"
                    },
                    "tier": {
                      "enum": [
                        "basic",
                        "developer",
                        "enterprise"
                      ],
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Authenticated liveness payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Authenticated liveness probe",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/ping/auth\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/ping/auth', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/ping/auth\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/ping/auth');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/plays": {
      "post": {
        "description": "Record a single log-level play for an ad you serve yourself (not through\nour programmatic pipes). The play lands in a dedicated NON-billing store\nand joins to (a) the creative classification you submitted via\nPOST /creatives — by `creative_ref` (content_sha256) — and (b) our\naudience moments captured on the screen during the play window. It then\nsurfaces through GET /analytics/content-performance and\n/analytics/creative-scenes for your screens.\n\nIdempotent on `play_id`. `screen_id` must be one of your registered\nearner screens. Measurement plays are never billed.\n",
        "operationId": "recordPartnerPlay",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerPlay"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "example": {
                  "content_sha256": "6c4b8f3e2a1d7b90c5e42f18a3d6b7e90124fa5c83b16d0e7f294a5c81b3d6e1",
                  "object": "partner_play",
                  "play_id": "play_20260115T093000Z_0d41",
                  "request_id": "req_9f2c4b8e1d7a63054c9e8f21b3d6a70e",
                  "status": "accepted"
                },
                "schema": {
                  "properties": {
                    "content_sha256": {
                      "type": "string"
                    },
                    "object": {
                      "example": "partner_play",
                      "type": "string"
                    },
                    "play_id": {
                      "type": "string"
                    },
                    "status": {
                      "enum": [
                        "accepted",
                        "duplicate"
                      ],
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Play accepted (or a duplicate of a prior play_id)."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "screen_id is not a screen registered to this partner."
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Record a log-level play",
        "tags": [
          "Measurement Plays"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/plays\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"screen_id\":\"<screen_id>\",\"creative_ref\":\"sha256:6c4b8f3e2a1d…e1\",\"play_id\":\"<play_id>\",\"completed_at\":\"2026-01-15T09:30:00.000Z\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/plays', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"screen_id\": \"<screen_id>\",\n    \"creative_ref\": \"sha256:6c4b8f3e2a1d…e1\",\n    \"play_id\": \"<play_id>\",\n    \"completed_at\": \"2026-01-15T09:30:00.000Z\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/plays\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"screen_id\": \"<screen_id>\",\n        \"creative_ref\": \"sha256:6c4b8f3e2a1d…e1\",\n        \"play_id\": \"<play_id>\",\n        \"completed_at\": \"2026-01-15T09:30:00.000Z\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/plays');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'screen_id' => '<screen_id>',\n        'creative_ref' => 'sha256:6c4b8f3e2a1d…e1',\n        'play_id' => '<play_id>',\n        'completed_at' => '2026-01-15T09:30:00.000Z',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/plays/batch": {
      "post": {
        "description": "Submit up to 500 plays in one request (each the same shape as POST /plays).\nEvery play is validated independently; the response reports per-play\nrejections (invalid_creative_ref, screen_not_owned).\n",
        "operationId": "recordPartnerPlaysBatch",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "plays": {
                    "items": {
                      "$ref": "#/components/schemas/PartnerPlay"
                    },
                    "maxItems": 500,
                    "minItems": 1,
                    "type": "array"
                  }
                },
                "required": [
                  "plays"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "example": {
                  "accepted": 1,
                  "duplicate": 1,
                  "errors": [
                    {
                      "play_id": "play_20260115T094500Z_7b02",
                      "reason": "screen_not_owned"
                    }
                  ],
                  "object": "partner_plays_batch_result",
                  "rejected": 1,
                  "request_id": "req_9f2c4b8e1d7a63054c9e8f21b3d6a70e",
                  "total": 3
                },
                "schema": {
                  "properties": {
                    "accepted": {
                      "type": "integer"
                    },
                    "duplicate": {
                      "type": "integer"
                    },
                    "errors": {
                      "items": {
                        "properties": {
                          "play_id": {
                            "nullable": true,
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "object": {
                      "example": "partner_plays_batch_result",
                      "type": "string"
                    },
                    "rejected": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Batch accepted; see per-play results."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Record a batch of log-level plays",
        "tags": [
          "Measurement Plays"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/plays/batch\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"plays\":[{\"screen_id\":\"<screen_id>\",\"creative_ref\":\"sha256:6c4b8f3e2a1d…e1\",\"play_id\":\"<play_id>\",\"completed_at\":\"2026-01-15T09:30:00.000Z\"}]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/plays/batch', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"plays\": [\n      {\n        \"screen_id\": \"<screen_id>\",\n        \"creative_ref\": \"sha256:6c4b8f3e2a1d…e1\",\n        \"play_id\": \"<play_id>\",\n        \"completed_at\": \"2026-01-15T09:30:00.000Z\"\n      }\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/plays/batch\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"plays\": [\n            {\n                \"screen_id\": \"<screen_id>\",\n                \"creative_ref\": \"sha256:6c4b8f3e2a1d…e1\",\n                \"play_id\": \"<play_id>\",\n                \"completed_at\": \"2026-01-15T09:30:00.000Z\",\n            },\n        ],\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/plays/batch');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'plays' => [\n            [\n                'screen_id' => '<screen_id>',\n                'creative_ref' => 'sha256:6c4b8f3e2a1d…e1',\n                'play_id' => '<play_id>',\n                'completed_at' => '2026-01-15T09:30:00.000Z',\n            ],\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/pricing": {
      "get": {
        "description": "Returns full pricing information for all Trillboards products including\ngraduated usage tiers, free tier thresholds, committed-use discounts,\nand billing setup URLs. No authentication required — designed for\nprogrammatic price discovery by AI agents and DSPs.\n\nResponse is cached for 1 hour (Cache-Control: public, max-age=3600).\n",
        "operationId": "getPricing",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "currency": "usd",
                  "description": "Pay-per-use with generous free tiers. No subscriptions, no commitments.",
                  "model": "usage_based",
                  "object": "pricing",
                  "request_id": "req_abc123"
                },
                "schema": {
                  "properties": {
                    "agent_register_url": {
                      "example": "/v1/partner/agent/register",
                      "type": "string"
                    },
                    "billing_setup_url": {
                      "example": "/v1/partner/billing/setup",
                      "type": "string"
                    },
                    "committed_use": {
                      "additionalProperties": {
                        "properties": {
                          "credits_usd": {
                            "type": "number"
                          },
                          "discount_pct": {
                            "type": "number"
                          },
                          "price_usd": {
                            "type": "number"
                          }
                        },
                        "type": "object"
                      },
                      "description": "Volume discount tiers for prepaid credits",
                      "type": "object"
                    },
                    "currency": {
                      "example": "usd",
                      "type": "string"
                    },
                    "description": {
                      "example": "Pay-per-use with generous free tiers. No subscriptions, no commitments.",
                      "type": "string"
                    },
                    "free_tiers": {
                      "description": "Free tier limits per product and meter",
                      "type": "object"
                    },
                    "model": {
                      "example": "usage_based",
                      "type": "string"
                    },
                    "object": {
                      "example": "pricing",
                      "type": "string"
                    },
                    "products": {
                      "description": "Pricing details per product",
                      "properties": {
                        "attribution": {
                          "properties": {
                            "description": {
                              "type": "string"
                            },
                            "meters": {
                              "additionalProperties": {
                                "properties": {
                                  "first_campaign_free": {
                                    "type": "boolean"
                                  },
                                  "per_unit": {
                                    "type": "string"
                                  },
                                  "unit_price_usd": {
                                    "type": "number"
                                  }
                                },
                                "type": "object"
                              },
                              "type": "object"
                            },
                            "name": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "data_api": {
                          "properties": {
                            "description": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "notes": {
                              "type": "string"
                            },
                            "tiers": {
                              "items": {
                                "properties": {
                                  "unit_price_usd": {
                                    "type": "number"
                                  },
                                  "up_to": {
                                    "description": "null means unlimited (final tier)",
                                    "nullable": true,
                                    "type": "integer"
                                  }
                                },
                                "type": "object"
                              },
                              "type": "array"
                            },
                            "unit": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "data_marketplace": {
                          "properties": {
                            "description": {
                              "type": "string"
                            },
                            "meters": {
                              "additionalProperties": {
                                "properties": {
                                  "free_quota_per_month": {
                                    "type": "integer"
                                  },
                                  "per_unit": {
                                    "type": "string"
                                  },
                                  "unit_price_usd": {
                                    "type": "number"
                                  }
                                },
                                "type": "object"
                              },
                              "type": "object"
                            },
                            "name": {
                              "type": "string"
                            },
                            "notes": {
                              "type": "string"
                            },
                            "revenue_share": {
                              "type": "object"
                            }
                          },
                          "type": "object"
                        },
                        "fein_edge_ai": {
                          "properties": {
                            "description": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "pricing": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "partner_platform": {
                          "properties": {
                            "description": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "pricing": {
                              "type": "string"
                            },
                            "revenue_share": {
                              "properties": {
                                "distributor": {
                                  "type": "number"
                                },
                                "earner": {
                                  "type": "number"
                                },
                                "platform": {
                                  "type": "number"
                                }
                              },
                              "type": "object"
                            }
                          },
                          "type": "object"
                        },
                        "programmatic": {
                          "properties": {
                            "description": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "pricing": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "proof_of_play": {
                          "properties": {
                            "description": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "notes": {
                              "type": "string"
                            },
                            "tiers": {
                              "items": {
                                "properties": {
                                  "unit_price_usd": {
                                    "type": "number"
                                  },
                                  "up_to": {
                                    "nullable": true,
                                    "type": "integer"
                                  }
                                },
                                "type": "object"
                              },
                              "type": "array"
                            },
                            "unit": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "request_id": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Pricing information",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "example": "public, max-age=3600",
                  "type": "string"
                }
              },
              "Link": {
                "schema": {
                  "example": "</.well-known/adagents.json>; rel=\"adcp-discovery\"",
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [],
        "summary": "Get machine-readable pricing",
        "tags": [
          "Billing"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/pricing\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/pricing');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/pricing\",\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/pricing');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/programmatic-event": {
      "post": {
        "deprecated": true,
        "description": "Backward-compatible alias for SDKs that send the device identifier in\nthe request body instead of the `/device/{deviceId}/programmatic-event`\npath. New integrations should use the device-scoped endpoint. Runtime\nstill accepts legacy `deviceId` bodies, but the documented schema uses\nsnake_case to keep generated SDK models unambiguous.\n",
        "operationId": "recordProgrammaticEventLegacyAlias",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "ad_id": {
                    "type": "string"
                  },
                  "device_id": {
                    "description": "Device fingerprint or external device ID",
                    "type": "string"
                  },
                  "did": {
                    "description": "Short legacy device identifier alias",
                    "type": "string"
                  },
                  "error_code": {
                    "type": "string"
                  },
                  "error_message": {
                    "type": "string"
                  },
                  "event": {
                    "enum": [
                      "ad_started",
                      "ad_ended",
                      "no_fill",
                      "error"
                    ],
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "request_id": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "ad_started",
                      "ad_ended",
                      "no_fill",
                      "error"
                    ],
                    "type": "string"
                  },
                  "variant_name": {
                    "type": "string"
                  }
                },
                "required": [
                  "event"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "success": true
                },
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Event recorded"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [],
        "summary": "Record programmatic ad event (legacy SDK alias)",
        "tags": [
          "Ads"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/programmatic-event\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event\":\"ad_started\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/programmatic-event', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"event\": \"ad_started\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/programmatic-event\",\n    json={\n        \"event\": \"ad_started\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/programmatic-event');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'event' => 'ad_started',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/quick-start": {
      "post": {
        "description": "Registers a new partner and creates their first device in a single API call.\nReturns API key, device credentials, SDK snippets, and configuration.\n\n**Rate limited:** 5 requests per hour per IP address.\n\n**Important:** The API key is only shown once in the response. Store it securely!\n",
        "operationId": "quickStart",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "company_name": {
                    "description": "Company or partner name",
                    "example": "Acme Screens Inc",
                    "type": "string"
                  },
                  "device_display": {
                    "description": "Display configuration for the first device",
                    "type": "object"
                  },
                  "device_location": {
                    "description": "Location for the first device",
                    "type": "object"
                  },
                  "device_name": {
                    "description": "Name for the first device",
                    "type": "string"
                  },
                  "email": {
                    "example": "developer@acme.com",
                    "format": "email",
                    "type": "string"
                  },
                  "partner_type": {
                    "default": "digital_signage",
                    "deprecated": true,
                    "description": "Optional legacy classification. Omit for the default self-serve Partner API key.",
                    "type": "string"
                  }
                },
                "required": [
                  "company_name",
                  "email"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "credentials": {
                      "api_key": "tb_prt_0000000000000000000000000000000000000000000000000000000000000000",
                      "api_key_prefix": "tb_prt_0000000000000...",
                      "warning": "Store this API key securely. It will not be shown again."
                    },
                    "device": {
                      "device_id": "660000000000000000000002",
                      "embed_url": "https://screen.trillboards.com?fp=P_5d41402abc4b2a76b9719d91",
                      "external_device_id": "qs_9f3c1a7b20d4",
                      "fingerprint": "P_5d41402abc4b2a76b9719d91",
                      "name": "Northwind Retail - Screen 1",
                      "screen_id": "660000000000000000000003",
                      "status": "approved"
                    },
                    "integration": {
                      "api_base": "https://api.trillboards.com/v1/partner",
                      "docs_url": "https://api.trillboards.com/v1/partner/docs",
                      "embed_url": "https://screen.trillboards.com?fp=P_5d41402abc4b2a76b9719d91",
                      "iframe_snippet": "<!-- Trillboards Screen Embed (simplest integration) -->\n<iframe\n  src=\"https://screen.trillboards.com?fp=P_5d41402abc4b2a76b9719d91\"\n  style=\"width:100%;height:100%;border:none;\"\n  allow=\"autoplay; fullscreen\"\n  allowfullscreen>\n</iframe>",
                      "sdk_snippet": "<!-- Trillboards Connect SDK -->\n<div id=\"trillboards-ad\" style=\"width:100%;height:100%;\"></div>\n<script src=\"https://unpkg.com/@trillboards/connect@latest/dist/trillboards-connect.global.js\"></script>\n<script>\n  var trillboards = TrillboardsConnect.init({\n    apiKey: 'tb_prt_0000000000000000000000000000000000000000000000000000000000000000',\n    deviceId: 'qs_9f3c1a7b20d4',\n    containerId: 'trillboards-ad',\n    options: {\n      heartbeatInterval: 60000,\n      adInterval: 60,\n      muted: true,\n      offlineCache: true,\n      analytics: true\n    }\n  });\n\n  trillboards.on('ready', function(data) {\n    console.log('Trillboards ready:', data.fingerprint);\n  });\n\n  trillboards.on('ad_start', function(data) {\n    console.log('Ad playing:', data.ad);\n  });\n</script>",
                      "sdk_url": "https://unpkg.com/@trillboards/connect@latest/dist/trillboards-connect.global.js"
                    },
                    "next_steps": [
                      "Add the SDK snippet or iframe to your page",
                      "Your device will auto-register and start showing ads",
                      "Monitor earnings at https://trillboards.com/earner",
                      "Connect Stripe for payouts: POST /v1/partner/stripe/connect",
                      "Register more devices: POST /v1/partner/device"
                    ],
                    "partner": {
                      "name": "Northwind Retail",
                      "partner_id": "660000000000000000000001",
                      "revenue_share_percent": 60,
                      "slug": "northwind-retail-9f3c1a",
                      "status": "active"
                    },
                    "sdk_config": {
                      "ad_interval": 60,
                      "allowed_ad_types": [
                        "video",
                        "image"
                      ],
                      "cache_size": 10,
                      "image_duration": 8,
                      "max_ads_per_hour": 20,
                      "overlay_button_text": "TAP TO CONTINUE",
                      "sound_enabled": false
                    }
                  },
                  "message": "Partner registered and first device created. You are ready to show ads.",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "credentials": {
                          "properties": {
                            "api_key": {
                              "description": "Store securely — shown only once!",
                              "example": "trb_partner_a1b2c3d4e5f6g7h8",
                              "type": "string"
                            },
                            "api_key_prefix": {
                              "type": "string"
                            },
                            "warning": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "device": {
                          "properties": {
                            "device_id": {
                              "type": "string"
                            },
                            "embed_url": {
                              "format": "uri",
                              "type": "string"
                            },
                            "external_device_id": {
                              "type": "string"
                            },
                            "fingerprint": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "screen_id": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "integration": {
                          "properties": {
                            "api_base": {
                              "type": "string"
                            },
                            "docs_url": {
                              "type": "string"
                            },
                            "embed_url": {
                              "format": "uri",
                              "type": "string"
                            },
                            "iframe_snippet": {
                              "type": "string"
                            },
                            "sdk_snippet": {
                              "type": "string"
                            },
                            "sdk_url": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "next_steps": {
                          "items": {
                            "type": "string"
                          },
                          "type": "array"
                        },
                        "partner": {
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "partner_id": {
                              "type": "string"
                            },
                            "revenue_share_percent": {
                              "type": "number"
                            },
                            "slug": {
                              "type": "string"
                            },
                            "status": {
                              "example": "active",
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "sdk_config": {
                          "properties": {
                            "ad_interval": {
                              "type": "integer"
                            },
                            "allowed_ad_types": {
                              "items": {
                                "type": "string"
                              },
                              "type": "array"
                            },
                            "cache_size": {
                              "type": "integer"
                            },
                            "image_duration": {
                              "type": "integer"
                            },
                            "max_ads_per_hour": {
                              "type": "integer"
                            },
                            "sound_enabled": {
                              "type": "boolean"
                            }
                          },
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Partner registered and first device created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing company_name or email"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Partner with this email already exists"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [],
        "summary": "Quick start — register partner and first device in one call",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/quick-start\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"company_name\":\"Acme Screens Inc\",\"email\":\"developer@acme.com\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/quick-start', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"company_name\": \"Acme Screens Inc\",\n    \"email\": \"developer@acme.com\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/quick-start\",\n    json={\n        \"company_name\": \"Acme Screens Inc\",\n        \"email\": \"developer@acme.com\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/quick-start');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'company_name' => 'Acme Screens Inc',\n        'email' => 'developer@acme.com',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/register": {
      "post": {
        "description": "Register once to receive a Partner API key.\nPartners are auto-activated for instant onboarding.\n\n**Important:** The API key is only shown once in the response. Store it securely!\n\nThe default flow is intentionally one-stop: send your organization name,\nslug, and contact email. Trillboards returns a Partner API key that can\nauthenticate partner account, device, ad-serving, impression, analytics,\nCMS, webhook, dashboard, earnings, and team-management endpoints in this\nspec.\n\n`partner_type` is optional and kept for backward compatibility with older\nintegrations. Most developers should omit it — it never forks the key\nyou get back. Every partner registers once and receives the same\nPartner API key, which already carries `sdk:*` for the CTV Measurement\ningestion surface and `mcp:tools` for capability-authorized AI tools.\n",
        "operationId": "registerPartner",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "allowed_domains": {
                    "description": "Whitelist of allowed referrer domains",
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "contact_email": {
                    "example": "developer@example.com",
                    "format": "email",
                    "type": "string"
                  },
                  "contact_name": {
                    "example": "Jane Developer",
                    "type": "string"
                  },
                  "name": {
                    "description": "Company or partner name",
                    "example": "Acme Vending Co",
                    "type": "string"
                  },
                  "partner_type": {
                    "default": "other",
                    "deprecated": true,
                    "description": "Optional legacy classification. Omit this field — it does\nnot change which API key you receive. Existing\nintegrations may continue sending historical values\n(including `ctv_publisher`) for backward compatibility;\nevery partner_type mints the same self-serve Partner API\nkey.\n",
                    "enum": [
                      "vending_machine",
                      "kiosk",
                      "digital_signage",
                      "retail_display",
                      "ctv_publisher",
                      "other"
                    ],
                    "example": "other",
                    "type": "string"
                  },
                  "revenue_share_percent": {
                    "description": "Partner's revenue share percentage (negotiated per partner)",
                    "maximum": 100,
                    "minimum": 0,
                    "type": "number"
                  },
                  "slug": {
                    "description": "URL-safe identifier (lowercase, no spaces)",
                    "example": "acmevending",
                    "type": "string"
                  }
                },
                "required": [
                  "name",
                  "slug",
                  "contact_email"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "api_key": "tb_prt_0000000000000000000000000000000000000000000000000000000000000000",
                    "partner_id": "660000000000000000000001",
                    "portal_access": {
                      "claim_complete_endpoint": "/v1/user/claim/complete",
                      "claim_email_status": "sent",
                      "claim_request_endpoint": "/v1/user/claim/request",
                      "claim_required": true,
                      "claim_verify_endpoint": "/v1/user/claim/verify",
                      "email": "partner@example.com",
                      "message": "An earner portal account has been created. Check your email to set your password.",
                      "new_account": true,
                      "portal_auth_url": "https://trillboards.com/earner/auth",
                      "portal_url": "https://trillboards.com/earner",
                      "user_id": "660000000000000000000099"
                    },
                    "revenue_share_percent": 60,
                    "slug": "northwind-retail",
                    "status": "active"
                  },
                  "message": "Partner registered successfully. API key shown once - save it securely!",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "api_key": {
                          "description": "Store this securely - only shown once. This ONE\nself-serve Partner API key authenticates the general\nPartner API surface in this spec AND the CTV\nMeasurement SDK ingestion surface (`/v2/sdk/*`,\n`/v2/csi`) — it carries `sdk:*` and `mcp:tools` by\ndefault, so no separate CTV or MCP key is issued. Reuse\nit during OpenRTB onboarding to add buyer capabilities.\n",
                          "example": "trb_partner_a1b2c3d4e5f6g7h8i9j0",
                          "type": "string"
                        },
                        "partner_id": {
                          "example": "507f1f77bcf86cd799439011",
                          "type": "string"
                        },
                        "portal_access": {
                          "description": "Optional portal onboarding details for the partner owner account",
                          "properties": {
                            "claim_complete_endpoint": {
                              "example": "/v1/user/claim/complete",
                              "type": "string"
                            },
                            "claim_request_endpoint": {
                              "example": "/v1/user/claim/request",
                              "type": "string"
                            },
                            "claim_required": {
                              "type": "boolean"
                            },
                            "claim_verify_endpoint": {
                              "example": "/v1/user/claim/verify",
                              "type": "string"
                            },
                            "email": {
                              "format": "email",
                              "type": "string"
                            },
                            "message": {
                              "example": "An earner portal account has been created. Check your email to set your password.",
                              "type": "string"
                            },
                            "new_account": {
                              "type": "boolean"
                            },
                            "portal_auth_url": {
                              "example": "https://trillboards.com/earner/auth",
                              "type": "string"
                            },
                            "portal_url": {
                              "example": "https://trillboards.com/earner",
                              "type": "string"
                            },
                            "user_id": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "revenue_share_percent": {
                          "example": 80,
                          "type": "number"
                        },
                        "slug": {
                          "example": "acmevending",
                          "type": "string"
                        },
                        "status": {
                          "enum": [
                            "pending",
                            "active"
                          ],
                          "example": "active",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "example": "Partner registered successfully. API key shown once - save it securely.",
                      "type": "string"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Partner registered successfully"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Partner slug already exists"
          }
        },
        "security": [],
        "summary": "Register a new partner",
        "tags": [
          "Partners"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/register\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Acme Vending Co\",\"slug\":\"acmevending\",\"contact_email\":\"developer@example.com\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/register', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"name\": \"Acme Vending Co\",\n    \"slug\": \"acmevending\",\n    \"contact_email\": \"developer@example.com\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/register\",\n    json={\n        \"name\": \"Acme Vending Co\",\n        \"slug\": \"acmevending\",\n        \"contact_email\": \"developer@example.com\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/register');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'name' => 'Acme Vending Co',\n        'slug' => 'acmevending',\n        'contact_email' => 'developer@example.com',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/sandbox/event": {
      "post": {
        "description": "Generates a realistic test event that can be used to validate webhook integrations.\nIf an SSE connection is active, the event is also delivered via SSE.\n\n**Supported event types:** `device.online`, `device.offline`, `impression.recorded`,\n`impression.verified`, `campaign.allocated`, `payout.processed`, `sensing.threshold_crossed`, `audience.spike`,\n`audience.demographics_update`, `audience.purchase_intent`, `audience.venue_busy`,\n`audience.context_update`, `venue.traffic_summary`, `auction.completed`, `bid.won`,\n`bid.lost`, `creative.performance`, `screen.anomaly_detected`\n",
        "operationId": "generateSandboxEvent",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "event_type": {
                    "description": "Event type to generate",
                    "example": "impression.recorded",
                    "type": "string"
                  },
                  "screen_id": {
                    "description": "Target sandbox screen (auto-selects first if omitted)",
                    "type": "string"
                  }
                },
                "required": [
                  "event_type"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "device_id": "sandbox_device_5f1c9a20",
                    "external_device_id": "ext_sandbox_device_5f1c9a20",
                    "fingerprint": "sandbox_fp_9d4b71f0c3a25e68",
                    "name": "Sandbox Screen 1",
                    "screen_id": "sandbox_screen_9a4c71e0d3b2",
                    "timestamp": "2026-08-31T14:22:31.004Z"
                  },
                  "event": "device.online",
                  "event_id": "test_evt_4b7e21c09df3",
                  "is_sandbox": true,
                  "object": "sandbox_event",
                  "screen_id": "sandbox_screen_9a4c71e0d3b2",
                  "sse_delivered": 1,
                  "timestamp": "2026-08-31T14:22:31.004Z"
                },
                "schema": {
                  "properties": {
                    "data": {
                      "description": "Event payload (varies by event type)",
                      "type": "object"
                    },
                    "event": {
                      "type": "string"
                    },
                    "event_id": {
                      "type": "string"
                    },
                    "is_sandbox": {
                      "example": true,
                      "type": "boolean"
                    },
                    "object": {
                      "example": "sandbox_event",
                      "type": "string"
                    },
                    "screen_id": {
                      "nullable": true,
                      "type": "string"
                    },
                    "sse_delivered": {
                      "description": "Number of SSE connections that received this event",
                      "type": "integer"
                    },
                    "timestamp": {
                      "format": "date-time",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Test event generated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing or invalid event_type"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Generate a test event",
        "tags": [
          "Sandbox"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/sandbox/event\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_type\":\"impression.recorded\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sandbox/event', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"event_type\": \"impression.recorded\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/sandbox/event\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"event_type\": \"impression.recorded\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sandbox/event');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'event_type' => 'impression.recorded',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/sandbox/reset": {
      "post": {
        "description": "Clears all sandbox screens and events for the partner.",
        "operationId": "resetSandbox",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "events_cleared": 17,
                  "object": "sandbox_reset",
                  "reset_at": "2026-08-31T14:25:02.640Z",
                  "screens_cleared": 2,
                  "success": true
                },
                "schema": {
                  "properties": {
                    "events_cleared": {
                      "type": "integer"
                    },
                    "object": {
                      "example": "sandbox_reset",
                      "type": "string"
                    },
                    "reset_at": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "screens_cleared": {
                      "type": "integer"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Sandbox reset"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reset sandbox data",
        "tags": [
          "Sandbox"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/sandbox/reset\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sandbox/reset', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/sandbox/reset\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sandbox/reset');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/sandbox/screen": {
      "post": {
        "description": "Creates a synthetic test screen with realistic data for integration testing.\nMaximum 10 sandbox screens per partner. Sandbox data expires after 24 hours.\n",
        "operationId": "createSandboxScreen",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "name": {
                    "description": "Custom screen name",
                    "type": "string"
                  },
                  "venue_type": {
                    "description": "Override venue type",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "audience": {
                    "avg_dwell_time_sec": 74,
                    "current_viewers": 9,
                    "demographics": {
                      "age_distribution": {
                        "18-24": 0.25,
                        "25-34": 0.35,
                        "35-44": 0.2,
                        "45-54": 0.12,
                        "55+": 0.08
                      },
                      "gender_split": {
                        "female": 0.48,
                        "male": 0.52
                      }
                    }
                  },
                  "created_at": "2026-08-31T14:20:44.118Z",
                  "device_id": "sandbox_device_5f1c9a20",
                  "display_settings": {
                    "ad_interval_seconds": 60,
                    "display_mode": "fullscreen",
                    "orientation": "landscape",
                    "resolution": "1920x1080"
                  },
                  "fingerprint": "sandbox_fp_9d4b71f0c3a25e68",
                  "id": "sandbox_screen_9a4c71e0d3b2",
                  "is_sandbox": true,
                  "last_heartbeat": "2026-08-31T14:20:44.118Z",
                  "location": {
                    "city": "New York",
                    "latitude": 40.758,
                    "longitude": -73.9855,
                    "name": "Times Square Kiosk",
                    "state": "NY",
                    "venue_type": "retail"
                  },
                  "name": "Sandbox Screen 1",
                  "object": "sandbox_screen",
                  "partner_id": "5f9d1a3c7e2b48d016c4a9f2",
                  "screen_id": "sandbox_screen_9a4c71e0d3b2",
                  "status": "online"
                },
                "schema": {
                  "properties": {
                    "audience": {
                      "properties": {
                        "avg_dwell_time_sec": {
                          "type": "number"
                        },
                        "current_viewers": {
                          "type": "integer"
                        },
                        "demographics": {
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "created_at": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "device_id": {
                      "type": "string"
                    },
                    "display_settings": {
                      "type": "object"
                    },
                    "fingerprint": {
                      "type": "string"
                    },
                    "id": {
                      "example": "sandbox_screen_abc123",
                      "type": "string"
                    },
                    "is_sandbox": {
                      "example": true,
                      "type": "boolean"
                    },
                    "last_heartbeat": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "location": {
                      "properties": {
                        "city": {
                          "type": "string"
                        },
                        "latitude": {
                          "type": "number"
                        },
                        "longitude": {
                          "type": "number"
                        },
                        "name": {
                          "type": "string"
                        },
                        "state": {
                          "type": "string"
                        },
                        "venue_type": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "name": {
                      "type": "string"
                    },
                    "object": {
                      "example": "sandbox_screen",
                      "type": "string"
                    },
                    "partner_id": {
                      "type": "string"
                    },
                    "screen_id": {
                      "type": "string"
                    },
                    "status": {
                      "example": "online",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Sandbox screen created"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Maximum 10 sandbox screens per partner"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a sandbox screen",
        "tags": [
          "Sandbox"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/sandbox/screen\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sandbox/screen', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/sandbox/screen\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={},\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sandbox/screen');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/sandbox/status": {
      "get": {
        "description": "Returns current sandbox screens, event counts, and recent activity.",
        "operationId": "getSandboxStatus",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "last_activity": "2026-08-31T14:22:31.004Z",
                  "object": "sandbox_status",
                  "recent_events": [
                    {
                      "event": "device.online",
                      "event_id": "test_evt_4b7e21c09df3",
                      "timestamp": "2026-08-31T14:22:31.004Z"
                    },
                    {
                      "event": "impression.recorded",
                      "event_id": "test_evt_1c9e40a7b285",
                      "timestamp": "2026-08-31T14:21:58.771Z"
                    }
                  ],
                  "sandbox_screens": [
                    {
                      "created_at": "2026-08-31T14:20:44.118Z",
                      "id": "sandbox_screen_9a4c71e0d3b2",
                      "last_heartbeat": "2026-08-31T14:24:14.118Z",
                      "location": {
                        "city": "New York",
                        "latitude": 40.758,
                        "longitude": -73.9855,
                        "name": "Times Square Kiosk",
                        "state": "NY",
                        "venue_type": "retail"
                      },
                      "name": "Sandbox Screen 1",
                      "status": "online"
                    }
                  ],
                  "total_screens": 1,
                  "total_test_events": 2
                },
                "schema": {
                  "properties": {
                    "last_activity": {
                      "format": "date-time",
                      "nullable": true,
                      "type": "string"
                    },
                    "object": {
                      "example": "sandbox_status",
                      "type": "string"
                    },
                    "recent_events": {
                      "items": {
                        "properties": {
                          "event": {
                            "type": "string"
                          },
                          "event_id": {
                            "type": "string"
                          },
                          "timestamp": {
                            "format": "date-time",
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "sandbox_screens": {
                      "items": {
                        "properties": {
                          "created_at": {
                            "format": "date-time",
                            "type": "string"
                          },
                          "id": {
                            "type": "string"
                          },
                          "last_heartbeat": {
                            "format": "date-time",
                            "type": "string"
                          },
                          "location": {
                            "type": "object"
                          },
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "total_screens": {
                      "type": "integer"
                    },
                    "total_test_events": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Sandbox status"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get sandbox status",
        "tags": [
          "Sandbox"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/sandbox/status\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sandbox/status', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/sandbox/status\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sandbox/status');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screen-groups": {
      "get": {
        "operationId": "listScreenGroups",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "has_more": false,
                    "limit": 20,
                    "object": "list",
                    "offset": 0,
                    "screen_groups": [
                      {
                        "created_at": "2026-08-24T14:02:11.482Z",
                        "description": "Every lobby screen across the NYC flagship stores.",
                        "id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                        "name": "Northwind Retail - NYC Lobbies",
                        "object": "screen_group",
                        "screen_count": 2,
                        "updated_at": "2026-08-24T14:02:11.482Z"
                      },
                      {
                        "created_at": "2026-08-19T09:41:03.117Z",
                        "description": null,
                        "id": "sg_9d41f7a2-0c53-4b8e-a1f6-72d0c4e59b17",
                        "name": "Northwind Retail - Checkout Endcaps",
                        "object": "screen_group",
                        "screen_count": 14,
                        "updated_at": "2026-08-23T18:12:44.905Z"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "has_more": {
                          "type": "boolean"
                        },
                        "object": {
                          "example": "list",
                          "type": "string"
                        },
                        "screen_groups": {
                          "items": {
                            "$ref": "#/components/schemas/ScreenGroup"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "A paginated list of screen groups"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List screen groups",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screen-groups\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screen-groups\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "description": "Create a reusable, named, partner-scoped set of screens. Optionally seed\nit with `screen_ids` (each must be a screen this partner owns). Gated by\nSCREEN_GROUPS_ENABLED.\n",
        "operationId": "createScreenGroup",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "screen_ids": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "required": [
                  "name"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "screen_group": {
                      "created_at": "2026-08-24T14:02:11.482Z",
                      "description": "Every lobby screen across the NYC flagship stores.",
                      "id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "name": "Northwind Retail - NYC Lobbies",
                      "object": "screen_group",
                      "screen_count": 2,
                      "updated_at": "2026-08-24T14:02:11.482Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "screen_group": {
                          "$ref": "#/components/schemas/ScreenGroup"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Screen group created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "One or more screens are not owned by this partner"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a screen group",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screen-groups\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"<name>\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"name\": \"<name>\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screen-groups\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"name\": \"<name>\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'name' => '<name>',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screen-groups/{groupId}": {
      "delete": {
        "operationId": "deleteScreenGroup",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "deleted": true,
                    "id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                    "object": "screen_group"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "deleted": {
                          "type": "boolean"
                        },
                        "id": {
                          "type": "string"
                        },
                        "object": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The screen group was archived"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete (archive) a screen group",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X DELETE \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.delete(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "get": {
        "operationId": "getScreenGroup",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "screen_group": {
                      "created_at": "2026-08-24T14:02:11.482Z",
                      "description": "Every lobby screen across the NYC flagship stores.",
                      "id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "name": "Northwind Retail - NYC Lobbies",
                      "object": "screen_group",
                      "screen_count": 2,
                      "updated_at": "2026-08-24T14:02:11.482Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "screen_group": {
                          "$ref": "#/components/schemas/ScreenGroup"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The screen group"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Retrieve a screen group",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "description": "Partial, non-clobbering update of name / description.",
        "operationId": "updateScreenGroup",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "screen_group": {
                      "created_at": "2026-08-24T14:02:11.482Z",
                      "description": "Every lobby screen across the NYC flagship stores.",
                      "id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "name": "Northwind Retail - NYC Lobbies (Q4)",
                      "object": "screen_group",
                      "screen_count": 2,
                      "updated_at": "2026-08-28T11:37:52.640Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "screen_group": {
                          "$ref": "#/components/schemas/ScreenGroup"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The updated screen group"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update a screen group",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={},\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screen-groups/{groupId}/analytics": {
      "get": {
        "description": "Fleet analytics (last 24h) scoped to the group's member screens. Same shape as\nGET /fleet/analytics. Gated by SCREEN_GROUPS_ENABLED.\n",
        "operationId": "getScreenGroupAnalytics",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "avg_attention": 0.412,
                    "avg_cpm": 3.85,
                    "fill_rate_pct": 62.44,
                    "group_id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                    "impressions_24h": 48213,
                    "screen_count": 14
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "additionalProperties": true,
                      "description": "Fleet analytics scoped to the group's live member screens — the same payload as GET /fleet/analytics (which also returns an average attention score alongside the fields below).",
                      "properties": {
                        "avg_cpm": {
                          "type": "number"
                        },
                        "fill_rate_pct": {
                          "type": "number"
                        },
                        "group_id": {
                          "type": "string"
                        },
                        "impressions_24h": {
                          "type": "integer"
                        },
                        "screen_count": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Group analytics"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get analytics for a screen group",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/analytics\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/analytics', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/analytics\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/analytics');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screen-groups/{groupId}/commands": {
      "post": {
        "description": "Fans a device command out across the group's member screens and returns a pollable\nfleet-command job (poll GET /fleet-commands/{id}). Max 500 member screens per command.\nGated by SCREEN_GROUPS_ENABLED.\n",
        "operationId": "createScreenGroupCommand",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "command": {
                    "properties": {
                      "payload": {
                        "type": "object"
                      },
                      "type": {
                        "enum": [
                          "reload",
                          "update_settings",
                          "restart",
                          "screenshot",
                          "clear_cache",
                          "update_content",
                          "refresh_ads",
                          "update_config",
                          "get_state",
                          "show_ad",
                          "play_ad"
                        ],
                        "type": "string"
                      }
                    },
                    "required": [
                      "type"
                    ],
                    "type": "object"
                  }
                },
                "required": [
                  "command"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "fleet_command": {
                      "command_type": "reload",
                      "completed_at": "2026-08-28T11:40:02.734Z",
                      "created_at": "2026-08-28T11:40:02.118Z",
                      "error": null,
                      "failed": 1,
                      "group_id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "id": "fc_6b0c2f19-84ad-4d33-b7e1-9f2a5c6d0e48",
                      "object": "fleet_command",
                      "queued": 1,
                      "results": [
                        {
                          "device_command_id": "6624f2b7c3d4e5f6a7b8cd01",
                          "error": null,
                          "screen_id": "6624f1a0c3d4e5f6a7b8ca11",
                          "status": "queued"
                        },
                        {
                          "device_command_id": null,
                          "error": "No device linked",
                          "screen_id": "6624f1a0c3d4e5f6a7b8ca12",
                          "status": "failed"
                        }
                      ],
                      "status": "completed",
                      "targeted": 2,
                      "updated_at": "2026-08-28T11:40:02.734Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "fleet_command": {
                          "$ref": "#/components/schemas/FleetCommand"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Fleet-command job created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Run a fleet command against a screen group",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/commands\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"command\":{\"type\":\"reload\"}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/commands', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"command\": {\n      \"type\": \"reload\"\n    }\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/commands\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"command\": {\n            \"type\": \"reload\",\n        },\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/commands');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'command' => [\n            'type' => 'reload',\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screen-groups/{groupId}/content-policy": {
      "get": {
        "description": "The typed content/moderation policy the group carries, or `null` when the\ngroup has none of its own. Member screens inherit it at read/serve time\n(screen ?? group ?? partner network default ?? system default). Gated by\nSCREEN_GROUPS_ENABLED.\n",
        "operationId": "getScreenGroupContentPolicy",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "content_policy": {
                      "allow_adult_content": false,
                      "allow_alcohol": false,
                      "allow_gambling": false,
                      "allow_political": false,
                      "allow_profanity": false,
                      "allow_religious": false,
                      "allow_tobacco": false,
                      "allow_violence": false,
                      "allowed_categories": [],
                      "allowed_keywords": [],
                      "allowed_subjects": [],
                      "auto_approve_known_brands": true,
                      "blocked_categories": [
                        "IAB25-3",
                        "IAB26"
                      ],
                      "blocked_iab_categories": [
                        "IAB25-3",
                        "IAB26"
                      ],
                      "blocked_keywords": [
                        "vape"
                      ],
                      "blocked_subjects": [
                        "firearms"
                      ],
                      "content_filtering_enabled": true,
                      "group_id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "max_age_rating": "PG",
                      "object": "screen_group_content_policy",
                      "preference_level": "moderate",
                      "updated_at": "2026-08-27T16:20:09.331Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "content_policy": {
                          "$ref": "#/components/schemas/ScreenGroupContentPolicy"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The group's content policy (nullable)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get a screen group's content policy",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/content-policy\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/content-policy', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/content-policy\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/content-policy');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "put": {
        "description": "Non-clobbering upsert — only the fields present in the body are written.\nRow existence = \"this group carries a content policy\". Busts the affected\nmember caches. Gated by SCREEN_GROUPS_ENABLED.\n",
        "operationId": "updateScreenGroupContentPolicy",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScreenGroupContentPolicyInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "content_policy": {
                      "allow_adult_content": false,
                      "allow_alcohol": false,
                      "allow_gambling": false,
                      "allow_political": false,
                      "allow_profanity": false,
                      "allow_religious": false,
                      "allow_tobacco": false,
                      "allow_violence": false,
                      "allowed_categories": [],
                      "allowed_keywords": [],
                      "allowed_subjects": [],
                      "auto_approve_known_brands": true,
                      "blocked_categories": [
                        "IAB25-3",
                        "IAB26"
                      ],
                      "blocked_iab_categories": [
                        "IAB25-3",
                        "IAB26"
                      ],
                      "blocked_keywords": [
                        "vape"
                      ],
                      "blocked_subjects": [
                        "firearms"
                      ],
                      "content_filtering_enabled": true,
                      "group_id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "max_age_rating": "G",
                      "object": "screen_group_content_policy",
                      "preference_level": "strict",
                      "updated_at": "2026-08-28T11:44:31.702Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "content_policy": {
                          "$ref": "#/components/schemas/ScreenGroupContentPolicy"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The updated content policy"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Set a screen group's content policy",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/content-policy\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"preference_level\":\"strict\",\"max_age_rating\":\"G\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/content-policy', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"preference_level\": \"strict\",\n    \"max_age_rating\": \"G\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/content-policy\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"preference_level\": \"strict\",\n        \"max_age_rating\": \"G\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/content-policy');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'preference_level' => 'strict',\n        'max_age_rating' => 'G',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screen-groups/{groupId}/members": {
      "delete": {
        "operationId": "removeScreenGroupMembers",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "screen_ids": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "required": [
                  "screen_ids"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "removed": 1
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "removed": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The remove result"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remove members from a screen group",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X DELETE \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"screen_ids\":[\"<screen_ids>\"]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"screen_ids\": [\n      \"<screen_ids>\"\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.delete(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"screen_ids\": [\n            \"<screen_ids>\",\n        ],\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'screen_ids' => [\n            '<screen_ids>',\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "get": {
        "operationId": "listScreenGroupMembers",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "data": [
                      {
                        "id": "6624f1a0c3d4e5f6a7b8ca11",
                        "object": "screen"
                      },
                      {
                        "id": "6624f1a0c3d4e5f6a7b8ca12",
                        "object": "screen"
                      }
                    ],
                    "has_more": false,
                    "limit": 100,
                    "object": "list",
                    "offset": 0
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "data": {
                          "items": {
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "object": {
                                "example": "screen",
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "has_more": {
                          "type": "boolean"
                        },
                        "object": {
                          "example": "list",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "A paginated list of member screens"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List a screen group's members",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "description": "Idempotent. Partial-success: screens not owned by this partner are\nreturned in `rejected` rather than failing the whole request.\n",
        "operationId": "addScreenGroupMembers",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "screen_ids": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "required": [
                  "screen_ids"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "added": 2,
                    "rejected": [
                      {
                        "id": "6624f1a0c3d4e5f6a7b8ca99",
                        "reason": "not_owned_by_partner"
                      },
                      {
                        "id": "6624f1a0c3d4e5f6a7b8cb04",
                        "reason": "device_not_paired"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "added": {
                          "type": "integer"
                        },
                        "rejected": {
                          "items": {
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "reason": {
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The add result"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Add members to a screen group",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"screen_ids\":[\"<screen_ids>\"]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"screen_ids\": [\n      \"<screen_ids>\"\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"screen_ids\": [\n            \"<screen_ids>\",\n        ],\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/members');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'screen_ids' => [\n            '<screen_ids>',\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screen-groups/{groupId}/sensing-config": {
      "get": {
        "description": "The group's sensing profile binding + typed performance overrides, or\n`null` when the group has none. Member screens inherit via the sensing\nresolver's group step when they have no screen-specific deployment. Gated\nby SCREEN_GROUPS_ENABLED.\n",
        "operationId": "getScreenGroupSensingConfig",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "sensing_config": {
                      "capture": {
                        "faceTriggeredIntervalMs": 10000,
                        "minCaptureIntervalMs": 5000,
                        "periodicIntervalMs": 60000,
                        "vehicleTriggeredIntervalMs": 20000
                      },
                      "capture_interval_ms": 15000,
                      "enabled": true,
                      "estimated_fps_impact": 12,
                      "group_id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "min_confidence": 0.4,
                      "object": "screen_group_sensing_config",
                      "profile_name": "retail_full_v1",
                      "report_interval_ms": 30000,
                      "updated_at": "2026-08-27T16:22:47.884Z",
                      "vision_budget": {
                        "screen_daily_calls": 2400,
                        "screen_hourly_pace": 150
                      }
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "sensing_config": {
                          "$ref": "#/components/schemas/ScreenGroupSensingConfig"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The group's sensing config (nullable)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get a screen group's sensing config",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/sensing-config\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/sensing-config', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/sensing-config\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/sensing-config');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "put": {
        "description": "Non-clobbering upsert. `profile_name` must resolve to a builtin or an\nexisting custom profile (else 400). Busts the affected member caches.\nGated by SCREEN_GROUPS_ENABLED.\n",
        "operationId": "updateScreenGroupSensingConfig",
        "parameters": [
          {
            "in": "path",
            "name": "groupId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScreenGroupSensingConfigInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "sensing_config": {
                      "capture": {
                        "minCaptureIntervalMs": 5000,
                        "periodicIntervalMs": 60000
                      },
                      "capture_interval_ms": 20000,
                      "enabled": true,
                      "estimated_fps_impact": 12,
                      "group_id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "min_confidence": 0.4,
                      "object": "screen_group_sensing_config",
                      "profile_name": "retail_full_v1",
                      "report_interval_ms": 30000,
                      "updated_at": "2026-08-28T11:46:12.049Z",
                      "vision_budget": {
                        "screen_daily_calls": 1200,
                        "screen_hourly_pace": null
                      }
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "sensing_config": {
                          "$ref": "#/components/schemas/ScreenGroupSensingConfig"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The updated sensing config"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Set a screen group's sensing config",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/sensing-config\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"enabled\":true}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/sensing-config', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"enabled\": true\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screen-groups/<groupId>/sensing-config\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"enabled\": True,\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen-groups/<groupId>/sensing-config');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'enabled' => true,\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screen/{external_code}/audience-segments": {
      "get": {
        "description": "Returns the OpenRTB 2.6 Supply-Side Audience (SDA) payload for the\nscreen, indexed by the partner-issued `external_code`. The payload\nencodes segment IDs from IAB Audience Taxonomy 1.1 and the\nTrillboards segtax=600 namespace, with current confidence values.\nBidders use this to apply per-screen targeting decisions.\n",
        "operationId": "getScreenAudienceSegments",
        "parameters": [
          {
            "description": "Partner-issued opaque screen identifier.",
            "in": "path",
            "name": "external_code",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "device": {
                    "ext": {
                      "dooh": {
                        "venuetypeid": 15
                      }
                    }
                  },
                  "ext": {
                    "trillboards": {
                      "freshness_seconds": 37,
                      "iab_taxonomy_version": "1.1",
                      "lookback_minutes": 15,
                      "moment_count": 42,
                      "screen_external_code": "NW-CONCOURSE-A-01",
                      "signal_derived": {
                        "ble_unique_devices_1h": 74,
                        "last_observed_at": "2026-08-31T14:00:00.000Z"
                      },
                      "trillboards_taxonomy_version": "1.0"
                    }
                  },
                  "user": {
                    "data": [
                      {
                        "ext": {
                          "segtax": 4
                        },
                        "name": "trillboards.com",
                        "segment": [
                          {
                            "id": "65"
                          },
                          {
                            "id": "223"
                          }
                        ]
                      },
                      {
                        "ext": {
                          "segtax": 600
                        },
                        "name": "trillboards.com",
                        "segment": [
                          {
                            "id": "tb:attire:archetype:athleisure"
                          },
                          {
                            "id": "tb:loyalty:weekly_regular:true"
                          },
                          {
                            "id": "tb:signal:ble_unique_devices_1h:25"
                          }
                        ]
                      }
                    ]
                  }
                },
                "schema": {
                  "properties": {
                    "external_code": {
                      "type": "string"
                    },
                    "segtax_4": {
                      "items": {
                        "properties": {
                          "confidence": {
                            "type": "number"
                          },
                          "id": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "segtax_600": {
                      "items": {
                        "properties": {
                          "confidence": {
                            "type": "number"
                          },
                          "id": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "OpenRTB SDA payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Screen not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "OpenRTB SDA payload for a screen",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screen/<external_code>/audience-segments\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screen/<external_code>/audience-segments', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screen/<external_code>/audience-segments\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screen/<external_code>/audience-segments');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/batch/content-preferences": {
      "post": {
        "description": "Update content preferences for multiple screens at once.\nCan apply custom preferences or a preset to up to 100 screens.\n",
        "operationId": "batchUpdateContentPreferences",
        "requestBody": {
          "content": {
            "application/json": {
              "examples": {
                "with_preferences": {
                  "summary": "Custom preferences",
                  "value": {
                    "preferences": {
                      "blocked_categories": [
                        "alcohol",
                        "gambling"
                      ]
                    },
                    "screen_ids": [
                      "screen1",
                      "screen2",
                      "screen3"
                    ]
                  }
                },
                "with_preset": {
                  "summary": "Apply preset",
                  "value": {
                    "preset_id": "family_friendly",
                    "screen_ids": [
                      "screen1",
                      "screen2",
                      "screen3"
                    ]
                  }
                }
              },
              "schema": {
                "properties": {
                  "preferences": {
                    "$ref": "#/components/schemas/ContentPreferencesInput"
                  },
                  "preset_id": {
                    "description": "Preset ID to apply (alternative to preferences)",
                    "type": "string"
                  },
                  "screen_ids": {
                    "description": "Array of screen IDs to update",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 100,
                    "type": "array"
                  }
                },
                "required": [
                  "screen_ids"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "errors": [
                    {
                      "error": "Screen not found or access denied",
                      "screen_id": "6507a1b2c3d4e5f6a7b8aa13"
                    }
                  ],
                  "failed": 1,
                  "object": "batch_result",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "total": 3,
                  "updated": 2,
                  "updated_screens": [
                    "6507a1b2c3d4e5f6a7b8aa11",
                    "6507a1b2c3d4e5f6a7b8aa12"
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/BatchResult"
                }
              }
            },
            "description": "Batch update completed"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Batch update content preferences",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/batch/content-preferences\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"screen_ids\":[\"screen1\",\"screen2\",\"screen3\"],\"preferences\":{\"blocked_categories\":[\"alcohol\",\"gambling\"]}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/batch/content-preferences', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"screen_ids\": [\n      \"screen1\",\n      \"screen2\",\n      \"screen3\"\n    ],\n    \"preferences\": {\n      \"blocked_categories\": [\n        \"alcohol\",\n        \"gambling\"\n      ]\n    }\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/batch/content-preferences\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"screen_ids\": [\n            \"screen1\",\n            \"screen2\",\n            \"screen3\",\n        ],\n        \"preferences\": {\n            \"blocked_categories\": [\n                \"alcohol\",\n                \"gambling\",\n            ],\n        },\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/batch/content-preferences');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'screen_ids' => [\n            'screen1',\n            'screen2',\n            'screen3',\n        ],\n        'preferences' => [\n            'blocked_categories' => [\n                'alcohol',\n                'gambling',\n            ],\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/batch/display-settings": {
      "post": {
        "description": "Update display settings for multiple screens (up to 100).",
        "operationId": "batchUpdateDisplaySettings",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "screen_ids": {
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 100,
                    "type": "array"
                  },
                  "settings": {
                    "$ref": "#/components/schemas/DisplaySettingsInput"
                  }
                },
                "required": [
                  "screen_ids",
                  "settings"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "errors": [
                    {
                      "code": "screen_not_found",
                      "error": "Screen not found or access denied",
                      "screen_id": "6507a1b2c3d4e5f6a7b8aa13"
                    }
                  ],
                  "failed": 1,
                  "object": "batch_result",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "total": 3,
                  "updated": 2,
                  "updated_screens": [
                    "6507a1b2c3d4e5f6a7b8aa11",
                    "6507a1b2c3d4e5f6a7b8aa12"
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/BatchResult"
                }
              }
            },
            "description": "Batch update completed"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Batch update display settings",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/batch/display-settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"screen_ids\":[\"<screen_ids>\"],\"settings\":{}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/batch/display-settings', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"screen_ids\": [\n      \"<screen_ids>\"\n    ],\n    \"settings\": {}\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/batch/display-settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"screen_ids\": [\n            \"<screen_ids>\",\n        ],\n        \"settings\": {},\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/batch/display-settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'screen_ids' => [\n            '<screen_ids>',\n        ],\n        'settings' => [],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/batch/programmatic-settings": {
      "post": {
        "description": "Update programmatic settings for multiple screens (up to 100).",
        "operationId": "batchUpdateProgrammaticSettings",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "screen_ids": {
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 100,
                    "type": "array"
                  },
                  "settings": {
                    "$ref": "#/components/schemas/ProgrammaticSettingsInput"
                  }
                },
                "required": [
                  "screen_ids",
                  "settings"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "errors": [],
                  "failed": 0,
                  "object": "batch_result",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "total": 2,
                  "updated": 2,
                  "updated_screens": [
                    "6507a1b2c3d4e5f6a7b8aa11",
                    "6507a1b2c3d4e5f6a7b8aa12"
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/BatchResult"
                }
              }
            },
            "description": "Batch update completed"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Batch update programmatic settings",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/batch/programmatic-settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"screen_ids\":[\"<screen_ids>\"],\"settings\":{}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/batch/programmatic-settings', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"screen_ids\": [\n      \"<screen_ids>\"\n    ],\n    \"settings\": {}\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/batch/programmatic-settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"screen_ids\": [\n            \"<screen_ids>\",\n        ],\n        \"settings\": {},\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/batch/programmatic-settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'screen_ids' => [\n            '<screen_ids>',\n        ],\n        'settings' => [],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/resolve": {
      "get": {
        "description": "Resolve a partner screen using a device identifier (fingerprint, device_id, or external_device_id).\nUse this when you only have device IDs but want to call screen-based CMS endpoints.\n",
        "operationId": "resolvePartnerScreen",
        "parameters": [
          {
            "description": "Partner device ID or fingerprint",
            "in": "query",
            "name": "device_id",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Partner external device ID (device_id from registration)",
            "in": "query",
            "name": "external_device_id",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Device fingerprint (fp)",
            "in": "query",
            "name": "fingerprint",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "device_id": "6507a1b2c3d4e5f6a7b8bb22",
                    "external_device_id": "machine-001",
                    "fingerprint": "P_a1b2c3d4e5f6a7b8",
                    "screen_id": "6507a1b2c3d4e5f6a7b8aa11"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "device_id": {
                          "type": "string"
                        },
                        "external_device_id": {
                          "type": "string"
                        },
                        "fingerprint": {
                          "type": "string"
                        },
                        "screen_id": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Screen resolved"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resolve screen ID by device identifier",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/resolve\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/resolve', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/resolve\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/resolve');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/analytics": {
      "get": {
        "description": "Get detailed analytics for a specific screen including impression trends,\ndaily timeline, and performance metrics over a configurable period.\n",
        "operationId": "getScreenAnalytics",
        "parameters": [
          {
            "description": "Screen ID",
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Time period for analytics",
            "in": "query",
            "name": "period",
            "schema": {
              "default": "7d",
              "enum": [
                "7d",
                "30d",
                "90d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "impressions": {
                      "daily_average": 1250,
                      "total": 8750,
                      "trend_percent": 12
                    },
                    "period": {
                      "days": 7,
                      "end": "2026-01-26T23:59:59.999Z",
                      "start": "2026-01-19T00:00:00.000Z"
                    },
                    "screen": {
                      "id": "507f1f77bcf86cd799439011",
                      "location": "Bloomington, MN",
                      "name": "Mall of America - Floor 2",
                      "online_status": "online",
                      "status": "active"
                    },
                    "timeline": [
                      {
                        "avg_duration_ms": 15200,
                        "date": "2026-01-20",
                        "impressions": 1100
                      },
                      {
                        "avg_duration_ms": 14800,
                        "date": "2026-01-21",
                        "impressions": 1300
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ScreenAnalyticsResponse"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Screen analytics retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Screen does not belong to this partner"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get per-screen analytics",
        "tags": [
          "Dashboard"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/analytics/atmosphere": {
      "get": {
        "description": "Returns the current venue atmosphere read for a screen — mood,\nnoise level, crowd density, current viewers, engagement. Tries\nlive data first, falls back to last hour of Postgres data.\nSame backing service as the earner endpoint.\n",
        "operationId": "getPartnerScreenAtmosphere",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "crowdDensity": "10-20",
                    "currentViewers": 6,
                    "dataAge": "live",
                    "engagement": 0.71,
                    "lastUpdated": "2026-08-31T14:22:07.481Z",
                    "mood": "happy",
                    "noiseLevel": "moderate"
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "crowdDensity": {
                          "nullable": true,
                          "type": "string"
                        },
                        "currentViewers": {
                          "nullable": true,
                          "type": "number"
                        },
                        "dataAge": {
                          "type": "string"
                        },
                        "engagement": {
                          "nullable": true,
                          "type": "number"
                        },
                        "lastUpdated": {
                          "nullable": true,
                          "type": "string"
                        },
                        "mood": {
                          "nullable": true,
                          "type": "string"
                        },
                        "noiseLevel": {
                          "nullable": true,
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Atmosphere payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Access denied for this screen"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Current atmosphere (mood, noise, crowd density)",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/atmosphere\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/atmosphere', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/atmosphere\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/atmosphere');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/analytics/intelligence": {
      "get": {
        "description": "Returns time-bucketed audience intelligence for a screen with\nLincoln-Petersen multi-screen dedup. Same backing service as the\nearner endpoint (`venueAnalyticsService.computeScreenIntelligence`);\nthe partner controller fetches the screen row so the service can\ndo its sibling-screen lookup the same way the earner does.\n",
        "operationId": "getPartnerScreenIntelligence",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "from",
            "required": true,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "to",
            "required": true,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "interval",
            "schema": {
              "default": "1h",
              "enum": [
                "5min",
                "15min",
                "1h",
                "1d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "dedup": {
                      "camera_count": 2,
                      "method": "lincoln_petersen",
                      "raw_total": 4127,
                      "unique_total": 3184
                    },
                    "from": "2026-08-24T00:00:00.000Z",
                    "interval": "1h",
                    "screen_id": "507f1f77bcf86cd799439011",
                    "summary": {
                      "avg_attention": 0.68,
                      "buckets": 168,
                      "total_unique_viewers": 3184
                    },
                    "timeseries": [
                      {
                        "age_distribution": {
                          "18-24": 0.21,
                          "25-34": 0.38,
                          "35-49": 0.29,
                          "50+": 0.12
                        },
                        "avg_attention": 0.71,
                        "avg_engagement": 0.63,
                        "avg_noise_level": -1,
                        "brand_mention_windows": 3,
                        "bucket": "2026-08-24T08:00:00.000Z",
                        "gender_distribution": {
                          "female": 0.46,
                          "male": 0.54
                        },
                        "income_level": "high",
                        "measurements": 312,
                        "mood": "happy",
                        "peak_viewers": 6,
                        "raw_viewers": 27,
                        "screen_count": 2,
                        "viewers": 21,
                        "weather": {
                          "condition": "clear",
                          "is_raining": false,
                          "temperature_f": 71.4
                        }
                      }
                    ],
                    "to": "2026-08-31T00:00:00.000Z"
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "dedup": {
                          "properties": {
                            "camera_count": {
                              "type": "integer"
                            },
                            "method": {
                              "type": "string"
                            },
                            "raw_total": {
                              "type": "integer"
                            },
                            "unique_total": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "from": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "interval": {
                          "type": "string"
                        },
                        "screen_id": {
                          "type": "string"
                        },
                        "summary": {
                          "properties": {
                            "avg_attention": {
                              "type": "number"
                            },
                            "buckets": {
                              "type": "integer"
                            },
                            "total_unique_viewers": {
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "timeseries": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "to": {
                          "format": "date-time",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Intelligence payload"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing or invalid from / to / interval, or date range exceeds 30 days"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Access denied for this screen"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Time-bucketed venue intelligence with Lincoln-Petersen dedup",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/intelligence?from=%3Cfrom%3E&to=%3Cto%3E\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/intelligence?from=%3Cfrom%3E&to=%3Cto%3E', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/intelligence?from=%3Cfrom%3E&to=%3Cto%3E\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/intelligence?from=%3Cfrom%3E&to=%3Cto%3E');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/analytics/taste-profile": {
      "get": {
        "description": "Returns the screen's FEIN Phase 6 taste profile — sample count,\nvenue type, daypart distribution, computed-from window, plus a\ntop-5 projection of creative categories that have played on this\nscreen recently. Same backing service as the earner endpoint\n(`screenTasteAnalyticsService.computeScreenTasteProfile`). Returns\n`ready: false` if the snapshotter hasn't filled the screen yet.\n",
        "operationId": "getPartnerScreenTasteProfile",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "computedAt": "2026-08-31T18:00:00.000Z",
                    "contentTypes": [
                      "default_stream",
                      "self_promo"
                    ],
                    "daypartDistribution": {
                      "afternoon": 0.28,
                      "evening": 0.17,
                      "midday": 0.34,
                      "morning": 0.21
                    },
                    "qualifyingReadings": 42,
                    "ready": true,
                    "sampleCount": 17,
                    "screenId": "507f1f77bcf86cd799439011",
                    "stable": true,
                    "tasteVersionHash": null,
                    "topCategories": [
                      {
                        "attentionWeight": 11,
                        "category": "Sports",
                        "lastPlayedAt": "2026-08-30T10:00:00.000Z",
                        "playCount": 38
                      },
                      {
                        "attentionWeight": 4,
                        "category": "Food & Drink",
                        "lastPlayedAt": "2026-08-29T16:30:00.000Z",
                        "playCount": 20
                      }
                    ],
                    "updatedAt": "2026-08-31T18:00:00.000Z",
                    "venueType": "transit",
                    "windowEnd": null,
                    "windowStart": null
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "computedAt": {
                          "format": "date-time",
                          "nullable": true,
                          "type": "string"
                        },
                        "daypartDistribution": {
                          "nullable": true,
                          "type": "object"
                        },
                        "ready": {
                          "type": "boolean"
                        },
                        "sampleCount": {
                          "type": "integer"
                        },
                        "screenId": {
                          "type": "string"
                        },
                        "tasteVersionHash": {
                          "nullable": true,
                          "type": "string"
                        },
                        "topCategories": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "updatedAt": {
                          "format": "date-time",
                          "nullable": true,
                          "type": "string"
                        },
                        "venueType": {
                          "nullable": true,
                          "type": "string"
                        },
                        "windowEnd": {
                          "format": "date-time",
                          "nullable": true,
                          "type": "string"
                        },
                        "windowStart": {
                          "format": "date-time",
                          "nullable": true,
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Taste profile payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Access denied for this screen"
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Taste profile temporarily unavailable"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Per-screen taste profile + top-5 creative categories",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/taste-profile\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/taste-profile', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/taste-profile\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/taste-profile');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/analytics/traffic": {
      "get": {
        "description": "Returns the per-hour foot-traffic histogram for a single screen\n(avg viewers, occupancy band, measurement count) plus a 0-10\nbusy-score. Same shape as\n`GET /v2/earner/screens/{screenId}/analytics/traffic` because both\nsurfaces delegate to `venueAnalyticsService.computeScreenTraffic`\n(extracted in PR #5265).\n",
        "operationId": "getPartnerScreenTraffic",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "days",
            "schema": {
              "default": 7,
              "maximum": 30,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "busyScore": 1.8,
                    "hourlyTraffic": [
                      {
                        "avgViewers": 2.5,
                        "hour": 8,
                        "measurements": 12,
                        "occupancy": "5-10"
                      },
                      {
                        "avgViewers": 8.1,
                        "hour": 18,
                        "measurements": 18,
                        "occupancy": "10-20"
                      }
                    ],
                    "periodDays": 7
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "busyScore": {
                          "type": "number"
                        },
                        "hourlyTraffic": {
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "periodDays": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Traffic payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Access denied for this screen"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Hourly foot-traffic patterns for a screen",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/traffic\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/traffic', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/traffic\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/traffic');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/analytics/vas": {
      "get": {
        "description": "Returns VAS summary statistics (weighted average, median, p90, face\ncount, attention score, measurement totals) plus the screen's\ncurrent CPM tier and tier multiplier. Same backing service as the\nearner endpoint (`venueAnalyticsService.computeScreenVAS`).\n",
        "operationId": "getPartnerScreenVAS",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "avgAttentionScore": 0.72,
                    "avgFaceCount": 6.3,
                    "avgVasWeighted": 4.5,
                    "lastMeasurement": "2026-08-31T12:00:00.000Z",
                    "medianVas": 3.8,
                    "multiplier": 2,
                    "p90Vas": 7.2,
                    "tier": "premium",
                    "totalMeasurements": 1500
                  },
                  "status": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "avgAttentionScore": {
                          "type": "number"
                        },
                        "avgFaceCount": {
                          "type": "number"
                        },
                        "avgVasWeighted": {
                          "type": "number"
                        },
                        "lastMeasurement": {
                          "nullable": true,
                          "type": "string"
                        },
                        "medianVas": {
                          "type": "number"
                        },
                        "multiplier": {
                          "type": "number"
                        },
                        "p90Vas": {
                          "type": "number"
                        },
                        "tier": {
                          "type": "string"
                        },
                        "totalMeasurements": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "status": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "VAS payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Access denied for this screen"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Verified Attention Seconds + CPM tier for a screen",
        "tags": [
          "Audience Intelligence"
        ],
        "x-beta": true,
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/vas\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/vas', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/vas\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/analytics/vas');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/apply-preset": {
      "post": {
        "description": "Apply a predefined content preset to a screen.\nThis overwrites existing content preferences with the preset values.\n",
        "operationId": "applyContentPreset",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "preset_id": "family_friendly"
              },
              "schema": {
                "properties": {
                  "preset_id": {
                    "description": "ID of the preset to apply",
                    "type": "string"
                  }
                },
                "required": [
                  "preset_id"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "allow_adult_content": false,
                  "allow_alcohol": false,
                  "allow_gambling": false,
                  "allow_political": false,
                  "allow_profanity": false,
                  "allow_religious": false,
                  "allow_tobacco": false,
                  "allow_violence": false,
                  "allowed_categories": [],
                  "allowed_keywords": [],
                  "allowed_subjects": [],
                  "auto_approve_known_brands": true,
                  "blocked_categories": [],
                  "blocked_keywords": [],
                  "blocked_subjects": [],
                  "content_filtering_enabled": true,
                  "max_age_rating": "G",
                  "object": "content_preferences",
                  "preference_level": "strict",
                  "preset_applied": "family-friendly",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "updated_at": "2026-08-30T14:12:07.480Z"
                },
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ContentPreferencesResponse"
                    },
                    {
                      "properties": {
                        "preset_applied": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    }
                  ]
                }
              }
            },
            "description": "Preset applied"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Apply content preset to screen",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/<screenId>/apply-preset\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"preset_id\":\"family_friendly\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/apply-preset', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"preset_id\": \"family_friendly\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/apply-preset\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"preset_id\": \"family_friendly\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/apply-preset');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'preset_id' => 'family_friendly',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/audience-metrics": {
      "get": {
        "description": "Get real-time audience metrics for a screen including face detection,\nattention tracking, and demographic signals from the Troveworks AI pipeline.\n",
        "operationId": "getAudienceMetrics",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Time period for metrics",
            "in": "query",
            "name": "period",
            "schema": {
              "default": "1h",
              "enum": [
                "1h",
                "6h",
                "24h"
              ],
              "type": "string"
            }
          },
          {
            "description": "Include historical data points",
            "in": "query",
            "name": "include_history",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "aggregated": {
                      "avgAttention": 0,
                      "avgDwellMs": 0,
                      "avgFaceCount": 0,
                      "groupComposition": {},
                      "incomeProfile": {
                        "high": 0,
                        "low": 0,
                        "medium": 0,
                        "premium": 0
                      }
                    },
                    "history": [],
                    "live": {
                      "avgAttention": 0,
                      "avgFaceCount": 0,
                      "incomeLevel": "unknown",
                      "maxFaces": 0,
                      "mood": "unknown",
                      "screenEngagement": "unknown"
                    },
                    "period": "1h",
                    "screen_id": "660000000000000000000003",
                    "screen_name": "Northwind Retail - Lobby",
                    "timestamp": "2026-08-25T17:42:11.503Z"
                  },
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/AudienceMetricsResponse"
                }
              }
            },
            "description": "Audience metrics retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get real-time audience metrics",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/audience-metrics\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/audience-metrics', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/audience-metrics\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/audience-metrics');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/content-preferences": {
      "get": {
        "description": "Get content filtering preferences for a specific screen.\nControls what types of ads can be displayed.\n",
        "operationId": "getContentPreferences",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "allow_adult_content": false,
                  "allow_alcohol": false,
                  "allow_gambling": false,
                  "allow_political": false,
                  "allow_profanity": false,
                  "allow_religious": false,
                  "allow_tobacco": false,
                  "allow_violence": false,
                  "allowed_categories": [],
                  "allowed_keywords": [],
                  "allowed_subjects": [],
                  "auto_approve_known_brands": true,
                  "blocked_categories": [
                    "alcohol",
                    "gambling"
                  ],
                  "blocked_keywords": [],
                  "blocked_subjects": [],
                  "content_filtering_enabled": true,
                  "max_age_rating": "PG",
                  "object": "content_preferences",
                  "preference_level": "moderate",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "updated_at": "2026-08-30T14:12:07.480Z"
                },
                "schema": {
                  "$ref": "#/components/schemas/ContentPreferencesResponse"
                }
              }
            },
            "description": "Content preferences retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get screen content preferences",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Partial update of content preferences. Only updates provided fields.\nTriggers real-time update to the screen via WebSocket.\n",
        "operationId": "patchContentPreferences",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContentPreferencesInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "allow_adult_content": false,
                  "allow_alcohol": true,
                  "allow_gambling": false,
                  "allow_political": false,
                  "allow_profanity": false,
                  "allow_religious": false,
                  "allow_tobacco": false,
                  "allow_violence": false,
                  "allowed_categories": [],
                  "allowed_keywords": [],
                  "allowed_subjects": [],
                  "auto_approve_known_brands": true,
                  "blocked_categories": [
                    "alcohol",
                    "gambling"
                  ],
                  "blocked_keywords": [],
                  "blocked_subjects": [],
                  "content_filtering_enabled": true,
                  "max_age_rating": "PG-13",
                  "object": "content_preferences",
                  "preference_level": "moderate",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "updated_at": "2026-08-30T14:12:07.480Z"
                },
                "schema": {
                  "$ref": "#/components/schemas/ContentPreferencesResponse"
                }
              }
            },
            "description": "Preferences updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update content preferences (partial)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"min_ad_rating\":\"G\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"min_ad_rating\": \"G\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"min_ad_rating\": \"G\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'min_ad_rating' => 'G',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "put": {
        "description": "Full update of content filtering preferences for a screen.\nReplaces all existing preferences with the provided values.\nTriggers real-time update to the screen via WebSocket.\n",
        "operationId": "updateContentPreferences",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContentPreferencesInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "allow_adult_content": false,
                  "allow_alcohol": false,
                  "allow_gambling": false,
                  "allow_political": false,
                  "allow_profanity": false,
                  "allow_religious": false,
                  "allow_tobacco": false,
                  "allow_violence": false,
                  "allowed_categories": [],
                  "allowed_keywords": [],
                  "allowed_subjects": [],
                  "auto_approve_known_brands": true,
                  "blocked_categories": [
                    "alcohol",
                    "gambling"
                  ],
                  "blocked_keywords": [],
                  "blocked_subjects": [],
                  "content_filtering_enabled": true,
                  "max_age_rating": "PG",
                  "object": "content_preferences",
                  "preference_level": "moderate",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "updated_at": "2026-08-30T14:12:07.480Z"
                },
                "schema": {
                  "$ref": "#/components/schemas/ContentPreferencesResponse"
                }
              }
            },
            "description": "Preferences updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update content preferences (full)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"min_ad_rating\":\"G\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"min_ad_rating\": \"G\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"min_ad_rating\": \"G\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/content-preferences');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'min_ad_rating' => 'G',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/default-streams": {
      "get": {
        "operationId": "listPartnerDefaultStreams",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "current": {
                      "_id": "65a3f0c1d2e3b4a5c6d7e8f9",
                      "batch_id": null,
                      "canva_design_id": null,
                      "canva_url": null,
                      "createdAt": "2026-08-30T14:11:58.204Z",
                      "description": "Background content",
                      "duration_seconds": null,
                      "embed_provider": null,
                      "embed_url": null,
                      "generic_iframe_url": null,
                      "google_slides_id": null,
                      "google_slides_url": null,
                      "iframe_config": null,
                      "image_duration": null,
                      "images": [],
                      "is_active": true,
                      "is_admin_created": false,
                      "is_currently_playing": true,
                      "last_played_at": "2026-08-30T14:12:07.480Z",
                      "latitude": 40.7128,
                      "local_file_path": null,
                      "location": "123 Example Ave, Springfield, IL",
                      "longitude": -74.006,
                      "name": "Lobby Ambience",
                      "play_count": 0,
                      "playback_order": 1,
                      "schedule_activated_at": null,
                      "schedule_completed_at": null,
                      "schedule_disabled_stream_ids": [],
                      "schedule_end_at": null,
                      "schedule_replace_existing": false,
                      "schedule_start_at": null,
                      "screenId": "6507a1b2c3d4e5f6a7b8aa11",
                      "social_wall_provider": null,
                      "social_wall_widget_id": null,
                      "source": "partner",
                      "status": "active",
                      "stream_type": "youtube",
                      "timezone": "America/New_York",
                      "timing_type": "immediate",
                      "transcode_status": null,
                      "twitch_content_type": null,
                      "twitch_embed_url": null,
                      "twitch_id": null,
                      "twitch_url": null,
                      "unique_id": null,
                      "updatedAt": "2026-08-30T14:12:07.480Z",
                      "userId": null,
                      "video_url": null,
                      "youtube_category_id": null,
                      "youtube_channel_title": null,
                      "youtube_description": null,
                      "youtube_duration_seconds": null,
                      "youtube_metadata_fetched_at": null,
                      "youtube_playlist_id": null,
                      "youtube_playlist_title": null,
                      "youtube_tags": null,
                      "youtube_thumbnail_url": null,
                      "youtube_title": null,
                      "youtube_url": "https://www.youtube.com/watch?v=EXAMPLEID01",
                      "youtube_view_count": null
                    },
                    "streams": [
                      {
                        "_id": "65a3f0c1d2e3b4a5c6d7e8f9",
                        "batch_id": null,
                        "canva_design_id": null,
                        "canva_url": null,
                        "createdAt": "2026-08-30T14:11:58.204Z",
                        "description": "Background content",
                        "duration_seconds": null,
                        "embed_provider": null,
                        "embed_url": null,
                        "generic_iframe_url": null,
                        "google_slides_id": null,
                        "google_slides_url": null,
                        "iframe_config": null,
                        "image_duration": null,
                        "images": [],
                        "is_active": true,
                        "is_admin_created": false,
                        "is_currently_playing": true,
                        "last_played_at": "2026-08-30T14:12:07.480Z",
                        "latitude": 40.7128,
                        "local_file_path": null,
                        "location": "123 Example Ave, Springfield, IL",
                        "longitude": -74.006,
                        "name": "Lobby Ambience",
                        "play_count": 0,
                        "playback_order": 1,
                        "schedule_activated_at": null,
                        "schedule_completed_at": null,
                        "schedule_disabled_stream_ids": [],
                        "schedule_end_at": null,
                        "schedule_replace_existing": false,
                        "schedule_start_at": null,
                        "screenId": "6507a1b2c3d4e5f6a7b8aa11",
                        "social_wall_provider": null,
                        "social_wall_widget_id": null,
                        "source": "partner",
                        "status": "active",
                        "stream_type": "youtube",
                        "timezone": "America/New_York",
                        "timing_type": "immediate",
                        "transcode_status": null,
                        "twitch_content_type": null,
                        "twitch_embed_url": null,
                        "twitch_id": null,
                        "twitch_url": null,
                        "unique_id": null,
                        "updatedAt": "2026-08-30T14:12:07.480Z",
                        "userId": null,
                        "video_url": null,
                        "youtube_category_id": null,
                        "youtube_channel_title": null,
                        "youtube_description": null,
                        "youtube_duration_seconds": null,
                        "youtube_metadata_fetched_at": null,
                        "youtube_playlist_id": null,
                        "youtube_playlist_title": null,
                        "youtube_tags": null,
                        "youtube_thumbnail_url": null,
                        "youtube_title": null,
                        "youtube_url": "https://www.youtube.com/watch?v=EXAMPLEID01",
                        "youtube_view_count": null
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "current": {
                          "$ref": "#/components/schemas/PartnerDefaultStream"
                        },
                        "streams": {
                          "items": {
                            "$ref": "#/components/schemas/PartnerDefaultStream"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Default streams list"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List default streams for a screen",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "operationId": "createPartnerDefaultStream",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerDefaultStreamCreate"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "_id": "65a3f0c1d2e3b4a5c6d7e8f9",
                    "batch_id": null,
                    "canva_design_id": null,
                    "canva_url": null,
                    "createdAt": "2026-08-30T14:11:58.204Z",
                    "description": "Background content",
                    "duration_seconds": null,
                    "embed_provider": null,
                    "embed_url": null,
                    "generic_iframe_url": null,
                    "google_slides_id": null,
                    "google_slides_url": null,
                    "iframe_config": null,
                    "image_duration": null,
                    "images": [],
                    "is_active": true,
                    "is_admin_created": false,
                    "is_currently_playing": true,
                    "last_played_at": "2026-08-30T14:12:07.480Z",
                    "latitude": 40.7128,
                    "local_file_path": null,
                    "location": "123 Example Ave, Springfield, IL",
                    "longitude": -74.006,
                    "name": "Lobby Ambience",
                    "play_count": 0,
                    "playback_order": 1,
                    "schedule_activated_at": null,
                    "schedule_completed_at": null,
                    "schedule_disabled_stream_ids": [],
                    "schedule_end_at": null,
                    "schedule_replace_existing": false,
                    "schedule_start_at": null,
                    "screenId": "6507a1b2c3d4e5f6a7b8aa11",
                    "social_wall_provider": null,
                    "social_wall_widget_id": null,
                    "source": "partner",
                    "status": "active",
                    "stream_type": "youtube",
                    "timezone": "America/New_York",
                    "timing_type": "immediate",
                    "transcode_status": null,
                    "twitch_content_type": null,
                    "twitch_embed_url": null,
                    "twitch_id": null,
                    "twitch_url": null,
                    "unique_id": null,
                    "updatedAt": "2026-08-30T14:12:07.480Z",
                    "userId": null,
                    "video_url": null,
                    "youtube_category_id": null,
                    "youtube_channel_title": null,
                    "youtube_description": null,
                    "youtube_duration_seconds": null,
                    "youtube_metadata_fetched_at": null,
                    "youtube_playlist_id": null,
                    "youtube_playlist_title": null,
                    "youtube_tags": null,
                    "youtube_thumbnail_url": null,
                    "youtube_title": null,
                    "youtube_url": "https://www.youtube.com/watch?v=EXAMPLEID01",
                    "youtube_view_count": null
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PartnerDefaultStream"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Default stream created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a default stream (URL-based)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"stream_type\":\"local\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"stream_type\": \"local\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"stream_type\": \"local\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'stream_type' => 'local',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/default-streams/images": {
      "post": {
        "operationId": "createPartnerDefaultStreamImages",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "image_duration": {
                    "description": "Seconds per image",
                    "type": "integer"
                  },
                  "images": {
                    "items": {
                      "format": "binary",
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "required": [
                  "images"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "_id": "65a3f0c1d2e3b4a5c6d7e8f9",
                    "batch_id": null,
                    "canva_design_id": null,
                    "canva_url": null,
                    "createdAt": "2026-08-30T14:11:58.204Z",
                    "description": "Image carousel content",
                    "duration_seconds": 20,
                    "embed_provider": null,
                    "embed_url": null,
                    "generic_iframe_url": null,
                    "google_slides_id": null,
                    "google_slides_url": null,
                    "iframe_config": null,
                    "image_duration": 10,
                    "images": [
                      "https://cdn.example.com/partner-streams/slide-1.jpg",
                      "https://cdn.example.com/partner-streams/slide-2.jpg"
                    ],
                    "is_active": true,
                    "is_admin_created": false,
                    "is_currently_playing": true,
                    "last_played_at": "2026-08-30T14:12:07.480Z",
                    "latitude": 40.7128,
                    "local_file_path": null,
                    "location": "123 Example Ave, Springfield, IL",
                    "longitude": -74.006,
                    "name": "Image Carousel (2 images)",
                    "play_count": 0,
                    "playback_order": 1,
                    "schedule_activated_at": null,
                    "schedule_completed_at": null,
                    "schedule_disabled_stream_ids": [],
                    "schedule_end_at": null,
                    "schedule_replace_existing": false,
                    "schedule_start_at": null,
                    "screenId": "6507a1b2c3d4e5f6a7b8aa11",
                    "social_wall_provider": null,
                    "social_wall_widget_id": null,
                    "source": "partner",
                    "status": "active",
                    "stream_type": "images",
                    "timezone": "America/New_York",
                    "timing_type": "immediate",
                    "transcode_status": null,
                    "twitch_content_type": null,
                    "twitch_embed_url": null,
                    "twitch_id": null,
                    "twitch_url": null,
                    "unique_id": null,
                    "updatedAt": "2026-08-30T14:12:07.480Z",
                    "userId": null,
                    "video_url": null,
                    "youtube_category_id": null,
                    "youtube_channel_title": null,
                    "youtube_description": null,
                    "youtube_duration_seconds": null,
                    "youtube_metadata_fetched_at": null,
                    "youtube_playlist_id": null,
                    "youtube_playlist_title": null,
                    "youtube_tags": null,
                    "youtube_thumbnail_url": null,
                    "youtube_title": null,
                    "youtube_url": null,
                    "youtube_view_count": null
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PartnerDefaultStream"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Default stream created"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Upload image carousel default stream",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/images\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -F \"images[]=@/path/to/images\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const { openAsBlob } = require('node:fs');\n\nconst form = new FormData();\nform.append('images[]', await openAsBlob('/path/to/images'));\n\nconst res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/images', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n  body: form,\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/images\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    files={\n        \"images[]\": open(\"/path/to/images\", \"rb\"),\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/images');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n    CURLOPT_POSTFIELDS => [\n        'images[]' => new CURLFile('/path/to/images'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/default-streams/order": {
      "put": {
        "operationId": "reorderPartnerDefaultStreams",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "order": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "required": [
                  "order"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "success": true
                },
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Default streams reordered"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Reorder default streams",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/order\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"order\":[\"<order>\"]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/order', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"order\": [\n      \"<order>\"\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/order\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"order\": [\n            \"<order>\",\n        ],\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/order');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'order' => [\n            '<order>',\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/default-streams/upload": {
      "post": {
        "operationId": "createPartnerDefaultStreamUpload",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "video": {
                    "format": "binary",
                    "type": "string"
                  }
                },
                "required": [
                  "video"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "_id": "65a3f0c1d2e3b4a5c6d7e8f9",
                    "batch_id": null,
                    "canva_design_id": null,
                    "canva_url": null,
                    "createdAt": "2026-08-30T14:11:58.204Z",
                    "description": "Uploaded video content",
                    "duration_seconds": null,
                    "embed_provider": null,
                    "embed_url": null,
                    "generic_iframe_url": null,
                    "google_slides_id": null,
                    "google_slides_url": null,
                    "iframe_config": null,
                    "image_duration": null,
                    "images": [],
                    "is_active": true,
                    "is_admin_created": false,
                    "is_currently_playing": true,
                    "last_played_at": "2026-08-30T14:12:07.480Z",
                    "latitude": 40.7128,
                    "local_file_path": null,
                    "location": "123 Example Ave, Springfield, IL",
                    "longitude": -74.006,
                    "name": "Lobby Loop",
                    "play_count": 0,
                    "playback_order": 1,
                    "schedule_activated_at": null,
                    "schedule_completed_at": null,
                    "schedule_disabled_stream_ids": [],
                    "schedule_end_at": null,
                    "schedule_replace_existing": false,
                    "schedule_start_at": null,
                    "screenId": "6507a1b2c3d4e5f6a7b8aa11",
                    "social_wall_provider": null,
                    "social_wall_widget_id": null,
                    "source": "partner",
                    "status": "active",
                    "stream_type": "uploaded",
                    "timezone": "America/New_York",
                    "timing_type": "immediate",
                    "transcode_status": null,
                    "twitch_content_type": null,
                    "twitch_embed_url": null,
                    "twitch_id": null,
                    "twitch_url": null,
                    "unique_id": null,
                    "updatedAt": "2026-08-30T14:12:07.480Z",
                    "userId": null,
                    "video_url": "https://cdn.example.com/partner-streams/lobby-loop.mp4",
                    "youtube_category_id": null,
                    "youtube_channel_title": null,
                    "youtube_description": null,
                    "youtube_duration_seconds": null,
                    "youtube_metadata_fetched_at": null,
                    "youtube_playlist_id": null,
                    "youtube_playlist_title": null,
                    "youtube_tags": null,
                    "youtube_thumbnail_url": null,
                    "youtube_title": null,
                    "youtube_url": null,
                    "youtube_view_count": null
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PartnerDefaultStream"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Default stream created"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Upload a video default stream",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/upload\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -F \"video=@/path/to/video\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const { openAsBlob } = require('node:fs');\n\nconst form = new FormData();\nform.append('video', await openAsBlob('/path/to/video'));\n\nconst res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/upload', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n  body: form,\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/upload\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    files={\n        \"video\": open(\"/path/to/video\", \"rb\"),\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/upload');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n    CURLOPT_POSTFIELDS => [\n        'video' => new CURLFile('/path/to/video'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/default-streams/{streamId}": {
      "delete": {
        "operationId": "deletePartnerDefaultStream",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "streamId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "success": true
                },
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Default stream deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete a default stream",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X DELETE \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/<streamId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/<streamId>', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.delete(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/<streamId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/<streamId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "put": {
        "operationId": "updatePartnerDefaultStream",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "streamId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerDefaultStreamUpdate"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "_id": "65a3f0c1d2e3b4a5c6d7e8f9",
                    "batch_id": null,
                    "canva_design_id": null,
                    "canva_url": null,
                    "createdAt": "2026-08-30T14:11:58.204Z",
                    "description": "Background content",
                    "duration_seconds": null,
                    "embed_provider": null,
                    "embed_url": null,
                    "generic_iframe_url": null,
                    "google_slides_id": null,
                    "google_slides_url": null,
                    "iframe_config": null,
                    "image_duration": null,
                    "images": [],
                    "is_active": true,
                    "is_admin_created": false,
                    "is_currently_playing": true,
                    "last_played_at": "2026-08-30T14:12:07.480Z",
                    "latitude": 40.7128,
                    "local_file_path": null,
                    "location": "123 Example Ave, Springfield, IL",
                    "longitude": -74.006,
                    "name": "Lobby Ambience (Winter)",
                    "play_count": 0,
                    "playback_order": 1,
                    "schedule_activated_at": null,
                    "schedule_completed_at": null,
                    "schedule_disabled_stream_ids": [],
                    "schedule_end_at": null,
                    "schedule_replace_existing": false,
                    "schedule_start_at": null,
                    "screenId": "6507a1b2c3d4e5f6a7b8aa11",
                    "social_wall_provider": null,
                    "social_wall_widget_id": null,
                    "source": "partner",
                    "status": "active",
                    "stream_type": "youtube",
                    "timezone": "America/New_York",
                    "timing_type": "immediate",
                    "transcode_status": null,
                    "twitch_content_type": null,
                    "twitch_embed_url": null,
                    "twitch_id": null,
                    "twitch_url": null,
                    "unique_id": null,
                    "updatedAt": "2026-08-30T14:12:07.480Z",
                    "userId": null,
                    "video_url": null,
                    "youtube_category_id": null,
                    "youtube_channel_title": null,
                    "youtube_description": null,
                    "youtube_duration_seconds": null,
                    "youtube_metadata_fetched_at": null,
                    "youtube_playlist_id": null,
                    "youtube_playlist_title": null,
                    "youtube_tags": null,
                    "youtube_thumbnail_url": null,
                    "youtube_title": null,
                    "youtube_url": "https://www.youtube.com/watch?v=EXAMPLEID02",
                    "youtube_view_count": null
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PartnerDefaultStream"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Default stream updated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update a default stream",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/<streamId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"stream_type\":\"local\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/<streamId>', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"stream_type\": \"local\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/<streamId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"stream_type\": \"local\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/default-streams/<streamId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'stream_type' => 'local',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/demographics": {
      "get": {
        "description": "Get demographic summary for a screen including age distribution,\ngender split, income profile, and lifestyle segments.\n",
        "operationId": "getScreenDemographics",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Time period",
            "in": "query",
            "name": "period",
            "schema": {
              "default": "7d",
              "enum": [
                "24h",
                "7d",
                "30d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "data_quality": {
                      "confidence": 0.9,
                      "sample_size": 1873
                    },
                    "demographics": {
                      "age_distribution": {
                        "18-25": 0,
                        "26-35": 0,
                        "36-50": 0,
                        "50+": 0
                      },
                      "confidence": 0.9,
                      "gender_split": {
                        "female": 0,
                        "male": 0
                      },
                      "group_composition": {
                        "couples": 0.24,
                        "families": 0.12,
                        "friends": 0.09,
                        "solo": 0.52,
                        "work_groups": 0.03
                      },
                      "income_profile": {
                        "high": 0.28,
                        "low": 0.18,
                        "medium": 0.45,
                        "premium": 0.09
                      },
                      "lifestyle_segments": [],
                      "sampleSize": 1873
                    },
                    "period": "7d",
                    "screen_id": "660000000000000000000003",
                    "screen_name": "Northwind Retail - Lobby",
                    "timestamp": "2026-08-25T17:42:11.503Z"
                  },
                  "request_id": "req_4f2a91c7e05b48d3a6b1c8e07f3d2a95",
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/DemographicsResponse"
                }
              }
            },
            "description": "Demographics retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get screen demographics",
        "tags": [
          "Analytics"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/demographics\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/demographics', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/demographics\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/demographics');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/display-settings": {
      "get": {
        "description": "Get display configuration for a screen including orientation,\nad surfaces, and playback policy.\n",
        "operationId": "getDisplaySettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ad_interval_seconds": 60,
                  "ad_surface_mode": null,
                  "advertisement_sizing": "cover",
                  "auto_fullscreen": true,
                  "content_fit": {
                    "advertisements": "fill",
                    "default_streams": "fill",
                    "self_promo": "fill"
                  },
                  "display_mode": "fullscreen",
                  "l_bar": {
                    "ad_duration": 8,
                    "ad_interval": 25,
                    "ads_enabled": true,
                    "enabled": false,
                    "position": "bottom",
                    "size_percent": 20
                  },
                  "object": "display_settings",
                  "pip": {
                    "aspect_ratio": 1.7777777777777777,
                    "enabled": false,
                    "margin_px": 24,
                    "position": "bottom-right",
                    "size_ratio": 0.3
                  },
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "screen_orientation": "landscape",
                  "sound_enabled": true,
                  "updated_at": "2026-08-30T14:12:07.480Z"
                },
                "schema": {
                  "$ref": "#/components/schemas/DisplaySettingsResponse"
                }
              }
            },
            "description": "Display settings retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get display settings",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Partial update of display settings.",
        "operationId": "patchDisplaySettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DisplaySettingsInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ad_interval_seconds": 90,
                  "ad_surface_mode": null,
                  "advertisement_sizing": "cover",
                  "auto_fullscreen": true,
                  "content_fit": {
                    "advertisements": "fill",
                    "default_streams": "fill",
                    "self_promo": "fill"
                  },
                  "display_mode": "fullscreen",
                  "l_bar": {
                    "ad_duration": 8,
                    "ad_interval": 25,
                    "ads_enabled": true,
                    "enabled": false,
                    "position": "bottom",
                    "size_percent": 20
                  },
                  "object": "display_settings",
                  "pip": {
                    "aspect_ratio": 1.7777777777777777,
                    "enabled": false,
                    "margin_px": 24,
                    "position": "bottom-right",
                    "size_ratio": 0.3
                  },
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "screen_orientation": "landscape",
                  "sound_enabled": true,
                  "updated_at": "2026-08-30T14:12:07.480Z"
                },
                "schema": {
                  "$ref": "#/components/schemas/DisplaySettingsResponse"
                }
              }
            },
            "description": "Settings updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update display settings (partial)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"orientation\":\"landscape\",\"display_mode\":\"fullscreen\",\"transition_effect\":\"fade\",\"idle_content\":\"default_stream\",\"qr_code_position\":\"bottom-left\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"orientation\": \"landscape\",\n    \"display_mode\": \"fullscreen\",\n    \"transition_effect\": \"fade\",\n    \"idle_content\": \"default_stream\",\n    \"qr_code_position\": \"bottom-left\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"orientation\": \"landscape\",\n        \"display_mode\": \"fullscreen\",\n        \"transition_effect\": \"fade\",\n        \"idle_content\": \"default_stream\",\n        \"qr_code_position\": \"bottom-left\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'orientation' => 'landscape',\n        'display_mode' => 'fullscreen',\n        'transition_effect' => 'fade',\n        'idle_content' => 'default_stream',\n        'qr_code_position' => 'bottom-left',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "put": {
        "description": "Full update of display settings for a screen.\nTriggers real-time update and screen refresh via WebSocket.\n",
        "operationId": "updateDisplaySettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DisplaySettingsInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "ad_interval_seconds": 60,
                  "ad_surface_mode": null,
                  "advertisement_sizing": "cover",
                  "auto_fullscreen": true,
                  "content_fit": {
                    "advertisements": "fill",
                    "default_streams": "fill",
                    "self_promo": "fill"
                  },
                  "display_mode": "fullscreen",
                  "l_bar": {
                    "ad_duration": 8,
                    "ad_interval": 25,
                    "ads_enabled": true,
                    "enabled": false,
                    "position": "bottom",
                    "size_percent": 20
                  },
                  "object": "display_settings",
                  "pip": {
                    "aspect_ratio": 1.7777777777777777,
                    "enabled": false,
                    "margin_px": 24,
                    "position": "bottom-right",
                    "size_ratio": 0.3
                  },
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "screen_orientation": "landscape",
                  "sound_enabled": true,
                  "updated_at": "2026-08-30T14:12:07.480Z"
                },
                "schema": {
                  "$ref": "#/components/schemas/DisplaySettingsResponse"
                }
              }
            },
            "description": "Settings updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update display settings (full)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"orientation\":\"landscape\",\"display_mode\":\"fullscreen\",\"transition_effect\":\"fade\",\"idle_content\":\"default_stream\",\"qr_code_position\":\"bottom-left\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"orientation\": \"landscape\",\n    \"display_mode\": \"fullscreen\",\n    \"transition_effect\": \"fade\",\n    \"idle_content\": \"default_stream\",\n    \"qr_code_position\": \"bottom-left\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"orientation\": \"landscape\",\n        \"display_mode\": \"fullscreen\",\n        \"transition_effect\": \"fade\",\n        \"idle_content\": \"default_stream\",\n        \"qr_code_position\": \"bottom-left\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/display-settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'orientation' => 'landscape',\n        'display_mode' => 'fullscreen',\n        'transition_effect' => 'fade',\n        'idle_content' => 'default_stream',\n        'qr_code_position' => 'bottom-left',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/l-bar": {
      "get": {
        "operationId": "getPartnerLBarSettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "display_preferences": {
                      "ad_surface_mode": null,
                      "display_mode": "l-bar",
                      "l_bar_ad_duration": 8,
                      "l_bar_ad_interval": 25,
                      "l_bar_ads_enabled": true,
                      "l_bar_position": "bottom",
                      "l_bar_size": 20,
                      "pip_settings": {
                        "aspect_ratio": 1.7777777777777777,
                        "enabled": false,
                        "margin": 24,
                        "position": "bottom-right",
                        "size_ratio": 0.3
                      }
                    },
                    "l_bar_content": {
                      "business_name": "Northwind Retail",
                      "contact_info": {
                        "address": "123 Example Ave, Springfield, IL",
                        "email": "store@example.com",
                        "phone": "+1 555 0100",
                        "website": "https://www.example.com",
                        "wifi_password": ""
                      },
                      "current_promotion": "Two-for-one cold brew until 4pm",
                      "custom_messages": [
                        "Ask about our loyalty card"
                      ],
                      "display_settings": {
                        "accent_color": "#4A90E2",
                        "background_color": "#000000",
                        "selected_sports_leagues": [],
                        "show_contact_info": false,
                        "show_date": true,
                        "show_social_media": false,
                        "show_sports_scores": false,
                        "show_stock_market": false,
                        "show_store_hours": true,
                        "show_time": true,
                        "show_weather": true,
                        "text_color": "#FFFFFF"
                      },
                      "enabled": true,
                      "live_updates": {
                        "custom_status": {
                          "enabled": false,
                          "status_message": ""
                        },
                        "queue_management": {
                          "current_number": 42,
                          "enabled": true,
                          "queue_message": "Now Serving"
                        },
                        "wait_time": {
                          "current_wait_minutes": 12,
                          "enabled": true,
                          "wait_message": "Current Wait Time"
                        }
                      },
                      "place_id": "",
                      "place_photo": "",
                      "place_url": "",
                      "rating": null,
                      "review_count": 0,
                      "social_media": {
                        "facebook": "https://www.facebook.com/example",
                        "google_reviews": "",
                        "instagram": "https://www.instagram.com/example",
                        "twitter": ""
                      },
                      "store_hours": {
                        "friday": "9:00 AM - 10:00 PM",
                        "monday": "9:00 AM - 9:00 PM",
                        "saturday": "10:00 AM - 10:00 PM",
                        "sunday": "11:00 AM - 6:00 PM",
                        "thursday": "9:00 AM - 9:00 PM",
                        "timezone": "America/New_York",
                        "tuesday": "9:00 AM - 9:00 PM",
                        "wednesday": "9:00 AM - 9:00 PM"
                      }
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PartnerLBarSettings"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "L-Bar settings"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get L-Bar settings",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "put": {
        "operationId": "updatePartnerLBarSettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "display_preferences": {
                    "$ref": "#/components/schemas/PartnerLBarDisplayPreferences"
                  },
                  "l_bar_content": {
                    "$ref": "#/components/schemas/PartnerLBarContent"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "ad_surfaces": [
                      {
                        "allowed_ad_types": [
                          "self_promo"
                        ],
                        "dimensions": {
                          "height": 1080,
                          "width": 1920
                        },
                        "display_area_ratio": 1,
                        "display_mode": "fullscreen",
                        "enabled": true,
                        "id": "fullscreen",
                        "surface_type": "fullscreen"
                      },
                      {
                        "allowed_ad_types": [
                          "paid",
                          "programmatic",
                          "header_bidding"
                        ],
                        "dimensions": {
                          "height": 270,
                          "width": 640
                        },
                        "display_area_ratio": 0.08333333333333333,
                        "display_mode": "l-bar",
                        "enabled": true,
                        "id": "lbar_slot_1",
                        "l_bar_position": "bottom",
                        "l_bar_size_percent": 25,
                        "layout": "horizontal",
                        "slot_count": 3,
                        "slot_index": 0,
                        "surface_group": "lbar",
                        "surface_type": "lbar_slot"
                      },
                      {
                        "allowed_ad_types": [
                          "paid",
                          "programmatic",
                          "header_bidding"
                        ],
                        "dimensions": {
                          "height": 270,
                          "width": 640
                        },
                        "display_area_ratio": 0.08333333333333333,
                        "display_mode": "l-bar",
                        "enabled": true,
                        "id": "lbar_slot_2",
                        "l_bar_position": "bottom",
                        "l_bar_size_percent": 25,
                        "layout": "horizontal",
                        "slot_count": 3,
                        "slot_index": 1,
                        "surface_group": "lbar",
                        "surface_type": "lbar_slot"
                      },
                      {
                        "allowed_ad_types": [
                          "paid",
                          "programmatic",
                          "header_bidding"
                        ],
                        "dimensions": {
                          "height": 270,
                          "width": 640
                        },
                        "display_area_ratio": 0.08333333333333333,
                        "display_mode": "l-bar",
                        "enabled": true,
                        "id": "lbar_slot_3",
                        "l_bar_position": "bottom",
                        "l_bar_size_percent": 25,
                        "layout": "horizontal",
                        "slot_count": 3,
                        "slot_index": 2,
                        "surface_group": "lbar",
                        "surface_type": "lbar_slot"
                      }
                    ],
                    "display_preferences": {
                      "ad_surface_mode": null,
                      "display_mode": "l-bar",
                      "l_bar_ad_duration": 8,
                      "l_bar_ad_interval": 25,
                      "l_bar_ads_enabled": true,
                      "l_bar_position": "bottom",
                      "l_bar_size": 20,
                      "pip_settings": {
                        "aspect_ratio": 1.7777777777777777,
                        "enabled": false,
                        "margin": 24,
                        "position": "bottom-right",
                        "size_ratio": 0.3
                      }
                    },
                    "l_bar_content": {
                      "business_name": "Northwind Retail",
                      "contact_info": {
                        "address": "123 Example Ave, Springfield, IL",
                        "email": "store@example.com",
                        "phone": "+1 555 0100",
                        "website": "https://www.example.com",
                        "wifi_password": ""
                      },
                      "current_promotion": "Two-for-one cold brew until 4pm",
                      "custom_messages": [
                        "Ask about our loyalty card"
                      ],
                      "display_settings": {
                        "accent_color": "#4A90E2",
                        "background_color": "#000000",
                        "selected_sports_leagues": [],
                        "show_contact_info": false,
                        "show_date": true,
                        "show_social_media": false,
                        "show_sports_scores": false,
                        "show_stock_market": false,
                        "show_store_hours": true,
                        "show_time": true,
                        "show_weather": true,
                        "text_color": "#FFFFFF"
                      },
                      "enabled": true,
                      "live_updates": {
                        "custom_status": {
                          "enabled": false,
                          "status_message": ""
                        },
                        "queue_management": {
                          "current_number": 42,
                          "enabled": true,
                          "queue_message": "Now Serving"
                        },
                        "wait_time": {
                          "current_wait_minutes": 12,
                          "enabled": true,
                          "wait_message": "Current Wait Time"
                        }
                      },
                      "place_id": "",
                      "place_photo": "",
                      "place_url": "",
                      "rating": null,
                      "review_count": 0,
                      "social_media": {
                        "facebook": "https://www.facebook.com/example",
                        "google_reviews": "",
                        "instagram": "https://www.instagram.com/example",
                        "twitter": ""
                      },
                      "store_hours": {
                        "friday": "9:00 AM - 10:00 PM",
                        "monday": "9:00 AM - 9:00 PM",
                        "saturday": "10:00 AM - 10:00 PM",
                        "sunday": "11:00 AM - 6:00 PM",
                        "thursday": "9:00 AM - 9:00 PM",
                        "timezone": "America/New_York",
                        "tuesday": "9:00 AM - 9:00 PM",
                        "wednesday": "9:00 AM - 9:00 PM"
                      }
                    },
                    "playback_policy": {
                      "image_ad_min_ms": 4000,
                      "image_ad_total_ms": 30000,
                      "lbar_settings": {
                        "ads_enabled": true,
                        "position": "bottom",
                        "size_percent": 20
                      },
                      "max_paid_share": null,
                      "paid_interval_ms": 30000,
                      "pip_settings": {
                        "aspect_ratio": 1.7777777777777777,
                        "enabled": false,
                        "margin": 24,
                        "position": "bottom-right",
                        "size_ratio": 0.3
                      },
                      "policy_version": "v1",
                      "schedule_policy": {
                        "fallback_to_slots": true,
                        "source": "date_range"
                      },
                      "self_promo_interval_ms": 60000,
                      "surface_policy": {
                        "header_bidding": [
                          "lbar"
                        ],
                        "paid": [
                          "fullscreen",
                          "lbar"
                        ],
                        "programmatic": [
                          "fullscreen",
                          "lbar"
                        ],
                        "self_promo": [
                          "fullscreen"
                        ]
                      }
                    }
                  },
                  "message": "L-Bar settings updated successfully",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PartnerLBarSettings"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "L-Bar settings updated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update L-Bar settings",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"display_preferences\":{}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"display_preferences\": {}\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"display_preferences\": {},\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'display_preferences' => [],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/l-bar/live-updates": {
      "put": {
        "operationId": "updatePartnerLBarLiveUpdates",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "custom_status": {
                    "type": "string"
                  },
                  "promotion": {
                    "type": "string"
                  },
                  "queue_number": {
                    "type": "string"
                  },
                  "wait_time_minutes": {
                    "type": "number"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "l_bar_content": {
                      "business_name": "Northwind Retail",
                      "contact_info": {
                        "address": "123 Example Ave, Springfield, IL",
                        "email": "store@example.com",
                        "phone": "+1 555 0100",
                        "website": "https://www.example.com",
                        "wifi_password": ""
                      },
                      "current_promotion": "Two-for-one cold brew until 4pm",
                      "custom_messages": [
                        "Ask about our loyalty card"
                      ],
                      "display_settings": {
                        "accent_color": "#4A90E2",
                        "background_color": "#000000",
                        "selected_sports_leagues": [],
                        "show_contact_info": false,
                        "show_date": true,
                        "show_social_media": false,
                        "show_sports_scores": false,
                        "show_stock_market": false,
                        "show_store_hours": true,
                        "show_time": true,
                        "show_weather": true,
                        "text_color": "#FFFFFF"
                      },
                      "enabled": true,
                      "live_updates": {
                        "custom_status": {
                          "enabled": false,
                          "status_message": ""
                        },
                        "queue_management": {
                          "current_number": 42,
                          "enabled": true,
                          "queue_message": "Now Serving"
                        },
                        "wait_time": {
                          "current_wait_minutes": 12,
                          "enabled": true,
                          "wait_message": "Current Wait Time"
                        }
                      },
                      "place_id": "",
                      "place_photo": "",
                      "place_url": "",
                      "rating": null,
                      "review_count": 0,
                      "social_media": {
                        "facebook": "https://www.facebook.com/example",
                        "google_reviews": "",
                        "instagram": "https://www.instagram.com/example",
                        "twitter": ""
                      },
                      "store_hours": {
                        "friday": "9:00 AM - 10:00 PM",
                        "monday": "9:00 AM - 9:00 PM",
                        "saturday": "10:00 AM - 10:00 PM",
                        "sunday": "11:00 AM - 6:00 PM",
                        "thursday": "9:00 AM - 9:00 PM",
                        "timezone": "America/New_York",
                        "tuesday": "9:00 AM - 9:00 PM",
                        "wednesday": "9:00 AM - 9:00 PM"
                      }
                    }
                  },
                  "message": "L-Bar live updates applied",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "L-Bar live updates applied"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update L-Bar live updates",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar/live-updates\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"queue_number\":\"<queue_number>\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar/live-updates', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"queue_number\": \"<queue_number>\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar/live-updates\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"queue_number\": \"<queue_number>\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/l-bar/live-updates');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'queue_number' => '<queue_number>',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/programmatic-settings": {
      "get": {
        "description": "Get programmatic/header bidding settings for a screen.\nControls Google IMA, Prebid, and other programmatic ad sources.\n",
        "operationId": "getProgrammaticSettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "blocked_advertisers": [],
                  "blocked_iab_categories": [],
                  "currency": "USD",
                  "dynamic_floor_pricing": false,
                  "enabled": true,
                  "floor_price_cpm": 0.5,
                  "object": "programmatic_settings",
                  "preferred_deal_ids": [],
                  "programmatic_allocation_percent": 100,
                  "provider": "trillboards_gam",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "timeout_ms": 1000,
                  "updated_at": "2026-08-30T14:12:07.480Z",
                  "vast_tag_url": null
                },
                "schema": {
                  "$ref": "#/components/schemas/ProgrammaticSettingsResponse"
                }
              }
            },
            "description": "Programmatic settings retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get programmatic settings",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Partial update of programmatic settings.",
        "operationId": "patchProgrammaticSettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProgrammaticSettingsInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "blocked_advertisers": [],
                  "blocked_iab_categories": [
                    "IAB25-6"
                  ],
                  "currency": "USD",
                  "dynamic_floor_pricing": false,
                  "enabled": false,
                  "floor_price_cpm": 2.5,
                  "object": "programmatic_settings",
                  "preferred_deal_ids": [],
                  "programmatic_allocation_percent": 30,
                  "provider": "trillboards_gam",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "timeout_ms": 1000,
                  "updated_at": "2026-08-30T14:12:07.480Z",
                  "vast_tag_url": null
                },
                "schema": {
                  "$ref": "#/components/schemas/ProgrammaticSettingsResponse"
                }
              }
            },
            "description": "Settings updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update programmatic settings (partial)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"priority\":\"direct_first\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"priority\": \"direct_first\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"priority\": \"direct_first\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'priority' => 'direct_first',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "put": {
        "description": "Full update of programmatic settings.",
        "operationId": "updateProgrammaticSettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProgrammaticSettingsInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "blocked_advertisers": [],
                  "blocked_iab_categories": [],
                  "currency": "USD",
                  "dynamic_floor_pricing": false,
                  "enabled": true,
                  "floor_price_cpm": 2.5,
                  "object": "programmatic_settings",
                  "preferred_deal_ids": [],
                  "programmatic_allocation_percent": 30,
                  "provider": "trillboards_gam",
                  "request_id": "req_9f2c4b7d1e8a3f60c5d2b9e17a4f6083",
                  "screen_id": "6507a1b2c3d4e5f6a7b8aa11",
                  "timeout_ms": 1000,
                  "updated_at": "2026-08-30T14:12:07.480Z",
                  "vast_tag_url": null
                },
                "schema": {
                  "$ref": "#/components/schemas/ProgrammaticSettingsResponse"
                }
              }
            },
            "description": "Settings updated"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update programmatic settings (full)",
        "tags": [
          "CMS"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PUT \"https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"priority\":\"direct_first\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings', {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"priority\": \"direct_first\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.put(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"priority\": \"direct_first\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/programmatic-settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PUT',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'priority' => 'direct_first',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/resolved-content-policy": {
      "get": {
        "description": "Introspection (\"why\"): the EFFECTIVE content policy for a screen and its\nprovenance. Whole-layer coalescing — the effective policy comes from\nexactly ONE layer (screen ?? group ?? partner network default ?? system\ndefault). Read-only preview. Gated by SCREEN_GROUPS_ENABLED.\n",
        "operationId": "getResolvedContentPolicy",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "effective": {
                      "allow_adult_content": false,
                      "allow_alcohol": false,
                      "allow_gambling": false,
                      "allow_political": false,
                      "allow_profanity": false,
                      "allow_religious": false,
                      "allow_tobacco": false,
                      "allow_violence": false,
                      "allowed_categories": [],
                      "allowed_keywords": [],
                      "allowed_subjects": [],
                      "auto_approve_known_brands": true,
                      "blocked_categories": [
                        "IAB25-3",
                        "IAB26"
                      ],
                      "blocked_iab_categories": [
                        "IAB25-3",
                        "IAB26"
                      ],
                      "blocked_keywords": [
                        "vape"
                      ],
                      "blocked_subjects": [
                        "firearms"
                      ],
                      "content_filtering_enabled": true,
                      "max_age_rating": "PG",
                      "preference_level": "moderate"
                    },
                    "object": "resolved_content_policy",
                    "provenance": {
                      "group_id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "source": "group"
                    },
                    "screen_id": "6624f1a0c3d4e5f6a7b8ca11"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ResolvedContentPolicy"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The effective content policy + provenance"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resolve the effective content policy for a screen",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/resolved-content-policy\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/resolved-content-policy', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/resolved-content-policy\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/resolved-content-policy');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/resolved-sensing-config": {
      "get": {
        "description": "Introspection: the EFFECTIVE sensing view for a screen and its provenance\n(screen deployment ?? group sensing config ?? default). Read-only preview.\nGated by SCREEN_GROUPS_ENABLED.\n",
        "operationId": "getResolvedSensingConfig",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "effective": {
                      "capture_interval_ms": 15000,
                      "estimated_fps_impact": 12,
                      "min_confidence": 0.4,
                      "profile_name": "retail_full_v1",
                      "report_interval_ms": 30000
                    },
                    "object": "resolved_sensing_config",
                    "provenance": {
                      "group_id": "sg_3f2b1c8e-7a4d-4e19-9c62-5b8f0a1d2e34",
                      "source": "group_default"
                    },
                    "screen_id": "6624f1a0c3d4e5f6a7b8ca11"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ResolvedSensingConfig"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The effective sensing config + provenance"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resolve the effective sensing config for a screen",
        "tags": [
          "Screen Groups"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/resolved-sensing-config\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/resolved-sensing-config', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/resolved-sensing-config\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/resolved-sensing-config');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/sensing/chips": {
      "get": {
        "description": "Returns the custom observation chips emitted by the screen's active Sense\nAnything profile(s). The returned field set is derived from the screen's\nprofile (the fields you defined via interpret-intent), bounded to custom\nprovenances. Scoped to a screen the authenticated partner owns.\n",
        "operationId": "getScreenSensingChips",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Lookback window in days (max 400)",
            "in": "query",
            "name": "days",
            "schema": {
              "default": 7,
              "type": "integer"
            }
          },
          {
            "description": "Max rows to return",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 1000,
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "fields": [
                      {
                        "description": "Whether the shopper is carrying a power tool",
                        "field_key": "carries_power_tool",
                        "type": "boolean"
                      }
                    ],
                    "rows": [
                      {
                        "field_key": "carries_power_tool",
                        "observed_at": "2026-06-06T18:50:45.803Z",
                        "provenance": "profile",
                        "value_bool": true
                      }
                    ],
                    "screen_id": "6a246ab97096f4a811d39469",
                    "total": 1,
                    "truncated": false
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "fields": {
                          "description": "The custom field_keys this screen senses (from its active profile)",
                          "items": {
                            "properties": {
                              "description": {
                                "type": "string"
                              },
                              "field_key": {
                                "type": "string"
                              },
                              "type": {
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "rows": {
                          "description": "Recent chip observations (one row per field_key per observation)",
                          "items": {
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "screen_id": {
                          "type": "string"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "truncated": {
                          "type": "boolean"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Custom sensing chips for the screen"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read the custom sensing chips a screen is producing",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/chips\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/chips', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/chips\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/chips');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/sensing/interpret-intent": {
      "post": {
        "description": "\"Sense Anything\" for partners. Given a natural-language intent, an LLM\ngenerates a custom sensing profile — a schema of custom fields the screen\nwill sense. This only GENERATES the profile (it is not persisted); POST the\nreturned profile to /screens/{screenId}/sensing/profile to deploy it.\nScoped to a screen the authenticated partner owns.\n",
        "operationId": "interpretScreenSensingIntent",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "deviceTier": "standard",
                "intent": "Count shoppers carrying power tools to the checkout"
              },
              "schema": {
                "properties": {
                  "deviceTier": {
                    "description": "Optional device-capability hint for model selection",
                    "type": "string"
                  },
                  "intent": {
                    "description": "Natural-language description of what to sense",
                    "type": "string"
                  }
                },
                "required": [
                  "intent"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "profile": {
                      "metrics_schema": [
                        {
                          "description": "Whether the shopper is carrying a power tool",
                          "field": "carries_power_tool",
                          "type": "boolean"
                        }
                      ],
                      "profile_name": "Power Tool Shoppers"
                    },
                    "screen_id": "6a246ab97096f4a811d39469"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "profile": {
                          "description": "The generated profile — pass to /sensing/profile to deploy",
                          "type": "object"
                        },
                        "screen_id": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Generated sensing profile (not yet deployed)"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing or empty intent"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Generate a custom sensing profile from a natural-language intent",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/interpret-intent\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"intent\":\"Count shoppers carrying power tools to the checkout\",\"deviceTier\":\"standard\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/interpret-intent', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"intent\": \"Count shoppers carrying power tools to the checkout\",\n    \"deviceTier\": \"standard\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/interpret-intent\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"intent\": \"Count shoppers carrying power tools to the checkout\",\n        \"deviceTier\": \"standard\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/interpret-intent');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'intent' => 'Count shoppers carrying power tools to the checkout',\n        'deviceTier' => 'standard',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/sensing/profile": {
      "post": {
        "description": "Persist + activate a custom sensing profile (from interpret-intent) on a\nscreen you own. Once active, the audience-analyze pipeline emits the\nprofile's custom chips for this screen's observations. Scoped to a screen\nthe authenticated partner owns.\n",
        "operationId": "deployScreenSensingProfile",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "profile": {
                  "metrics_schema": [
                    {
                      "description": "Whether the shopper is carrying a power tool",
                      "field": "carries_power_tool",
                      "type": "boolean"
                    }
                  ],
                  "profile_name": "Power Tool Shoppers"
                }
              },
              "schema": {
                "properties": {
                  "profile": {
                    "description": "The sensing profile object returned by interpret-intent",
                    "type": "object"
                  }
                },
                "required": [
                  "profile"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "command": {
                      "id": "cmd_xyz789",
                      "type": "DEPLOY_SENSING_PROFILE"
                    },
                    "profile": {
                      "active": true,
                      "profile_id": "sp_abc123",
                      "profile_name": "Power Tool Shoppers"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "command": {
                          "type": "object"
                        },
                        "profile": {
                          "type": "object"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Profile deployed and pushed to the device"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing or invalid profile"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Device fingerprint could not be resolved for this screen"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Deploy a custom sensing profile to a screen",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/profile\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"profile\":{\"profile_name\":\"Power Tool Shoppers\",\"metrics_schema\":[{\"field\":\"carries_power_tool\",\"type\":\"boolean\",\"description\":\"Whether the shopper is carrying a power tool\"}]}}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/profile', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"profile\": {\n      \"profile_name\": \"Power Tool Shoppers\",\n      \"metrics_schema\": [\n        {\n          \"field\": \"carries_power_tool\",\n          \"type\": \"boolean\",\n          \"description\": \"Whether the shopper is carrying a power tool\"\n        }\n      ]\n    }\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/profile\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"profile\": {\n            \"profile_name\": \"Power Tool Shoppers\",\n            \"metrics_schema\": [\n                {\n                    \"field\": \"carries_power_tool\",\n                    \"type\": \"boolean\",\n                    \"description\": \"Whether the shopper is carrying a power tool\",\n                },\n            ],\n        },\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/profile');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'profile' => [\n            'profile_name' => 'Power Tool Shoppers',\n            'metrics_schema' => [\n                [\n                    'field' => 'carries_power_tool',\n                    'type' => 'boolean',\n                    'description' => 'Whether the shopper is carrying a power tool',\n                ],\n            ],\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/screens/{screenId}/sensing/settings": {
      "get": {
        "description": "Returns the screen's desired/effective sensing performance mode, active\ncadence, read-only device capability telemetry, safety warnings, and the\nlast applied sensing profile. Device hardware facts are telemetry-derived\nand cannot be overwritten through this API.\n",
        "operationId": "getScreenSensingSettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "active_profile": {
                      "capabilities": [
                        "viewer_count"
                      ],
                      "capture_interval_ms": 20000,
                      "profile_name": "Viewer Count Only",
                      "report_interval_ms": 30000
                    },
                    "desired_mode": "presence_only",
                    "device_capability": {
                      "gpu_delegate_supported": false,
                      "model_tier": "FLOOR",
                      "soc_vendor": "rockchip",
                      "telemetry_fresh": true
                    },
                    "effective_mode": "presence_only",
                    "safety": {
                      "allowed": true,
                      "blocks": [],
                      "warnings": []
                    },
                    "screen_id": "69af173ddea9a4adc44d1f62"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/SensingSettingsResponse"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Sensing performance settings for the screen"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Read a screen's AI sensing performance settings",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/settings', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Applies a high-level sensing performance mode to a screen you own. The\nserver translates the mode into the canonical sensing profile, cadence,\nprogram spec, and device command. Use this instead of editing raw model\nids, cadence, or device tier.\n",
        "operationId": "updateScreenSensingSettings",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "performance_mode": "presence_only"
              },
              "schema": {
                "properties": {
                  "performance_mode": {
                    "$ref": "#/components/schemas/SensingPerformanceMode"
                  }
                },
                "required": [
                  "performance_mode"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "command": {
                          "type": "object"
                        },
                        "profile": {
                          "type": "object"
                        },
                        "settings": {
                          "$ref": "#/components/schemas/SensingSettingsResponse"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Sensing performance mode applied through the profile pipeline"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Unsupported performance mode"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Device identity unresolved or safety checks blocked the requested workload"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update a screen's AI sensing performance mode",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/settings\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"performance_mode\":\"presence_only\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/settings', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"performance_mode\": \"presence_only\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/settings\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"performance_mode\": \"presence_only\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/screens/<screenId>/sensing/settings');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'performance_mode' => 'presence_only',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/sensing/routes": {
      "get": {
        "description": "List the Routes owned by the authenticated partner.",
        "operationId": "listPartnerSensingRoutes",
        "parameters": [
          {
            "description": "Include disabled routes. Pass `false` to list only enabled routes (default true).",
            "in": "query",
            "name": "includeDisabled",
            "schema": {
              "default": true,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "routes": [
                      {
                        "actions": [
                          {
                            "actionId": "ra_2c7e40b1d9f8",
                            "attachClip": true,
                            "channel": "whatsapp",
                            "clipDurationSeconds": 30,
                            "enabled": true,
                            "recipientE164": "+12025550123",
                            "recipientEmail": null,
                            "routeId": "rt_8f21c0a94b6d47e3",
                            "slackChannelId": null,
                            "templateName": "tb_alert_threshold",
                            "webhookId": null
                          }
                        ],
                        "activeDaysMask": 127,
                        "activeEndMinute": 360,
                        "activeStartMinute": 1200,
                        "activeTz": "America/New_York",
                        "coalesceWindowSeconds": 0,
                        "condition": {
                          "field": "loiter_count",
                          "op": "gt",
                          "value": 0
                        },
                        "confirmSamples": null,
                        "confirmWindowSeconds": null,
                        "createdAt": "2026-08-30T14:11:58.204Z",
                        "createdBy": "partner:6507a1b2c3d4e5f6a7b8cc33",
                        "debounceSeconds": 300,
                        "deletedAt": null,
                        "digest": null,
                        "enabled": true,
                        "entityMemorySeconds": null,
                        "entityMinDwellSeconds": null,
                        "escalationStepNum": null,
                        "forceRealtime": false,
                        "mode": "threshold",
                        "nagIntervalSeconds": null,
                        "name": "Loitering after 8pm",
                        "ownerId": "6507a1b2c3d4e5f6a7b8cc33",
                        "ownerKind": "partner",
                        "partnerId": "6507a1b2c3d4e5f6a7b8cc33",
                        "profileName": "parking_watch",
                        "reFirePolicy": "once_per_episode",
                        "routeId": "rt_8f21c0a94b6d47e3",
                        "screenFocusRegions": [
                          {
                            "height": 0.6,
                            "screenMongoId": "507f1f77bcf86cd799439011",
                            "width": 0.5,
                            "x": 0.1,
                            "y": 0.2
                          }
                        ],
                        "screenMongoIds": [
                          "507f1f77bcf86cd799439011"
                        ],
                        "sustainMinSamples": 1,
                        "sustainSeconds": null,
                        "sustainWindowSeconds": 120,
                        "targetGroupId": null,
                        "updatedAt": "2026-08-30T14:12:07.480Z"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "routes": {
                          "items": {
                            "$ref": "#/components/schemas/SensingRoute"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Your routes"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List your Sense Anything Routes",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/sensing/routes\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sensing/routes', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/sensing/routes\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sensing/routes');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "description": "Create a Route: when a sensed field crosses a threshold on a set of your\nscreens (threshold mode), or on a fixed schedule (digest mode), fire one or\nmore channel actions (webhook / managed WhatsApp / SMS / Slack / email).\nEvery screen in `screenMongoIds` must be a screen this partner owns — a\nforeign screen returns 403. Anti-noise (sustain/debounce) and an active\nwindow are first-class. Gated by the platform `SENSING_ROUTES_ENABLED`\nflag (404 when the feature is disabled).\n",
        "operationId": "createPartnerSensingRoute",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePartnerSensingRouteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "route": {
                      "actions": [
                        {
                          "actionId": "ra_2c7e40b1d9f8",
                          "attachClip": true,
                          "channel": "whatsapp",
                          "clipDurationSeconds": 30,
                          "enabled": true,
                          "recipientE164": "+12025550123",
                          "recipientEmail": null,
                          "routeId": "rt_8f21c0a94b6d47e3",
                          "slackChannelId": null,
                          "templateName": "tb_alert_threshold",
                          "webhookId": null
                        }
                      ],
                      "activeDaysMask": 127,
                      "activeEndMinute": 360,
                      "activeStartMinute": 1200,
                      "activeTz": "America/New_York",
                      "coalesceWindowSeconds": 0,
                      "condition": {
                        "field": "loiter_count",
                        "op": "gt",
                        "value": 0
                      },
                      "confirmSamples": null,
                      "confirmWindowSeconds": null,
                      "createdAt": "2026-08-30T14:11:58.204Z",
                      "createdBy": "partner:6507a1b2c3d4e5f6a7b8cc33",
                      "debounceSeconds": 300,
                      "deletedAt": null,
                      "digest": null,
                      "enabled": true,
                      "entityMemorySeconds": null,
                      "entityMinDwellSeconds": null,
                      "escalationStepNum": null,
                      "forceRealtime": false,
                      "mode": "threshold",
                      "nagIntervalSeconds": null,
                      "name": "Loitering after 8pm",
                      "ownerId": "6507a1b2c3d4e5f6a7b8cc33",
                      "ownerKind": "partner",
                      "partnerId": "6507a1b2c3d4e5f6a7b8cc33",
                      "profileName": "parking_watch",
                      "reFirePolicy": "once_per_episode",
                      "routeId": "rt_8f21c0a94b6d47e3",
                      "screenFocusRegions": [
                        {
                          "height": 0.6,
                          "screenMongoId": "507f1f77bcf86cd799439011",
                          "width": 0.5,
                          "x": 0.1,
                          "y": 0.2
                        }
                      ],
                      "screenMongoIds": [
                        "507f1f77bcf86cd799439011"
                      ],
                      "sustainMinSamples": 1,
                      "sustainSeconds": null,
                      "sustainWindowSeconds": 120,
                      "targetGroupId": null,
                      "updatedAt": "2026-08-30T14:12:07.480Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "route": {
                          "$ref": "#/components/schemas/SensingRoute"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Route created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "One or more screens are not owned by this partner"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create a Sense Anything Route (threshold or scheduled-digest alert)",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/sensing/routes\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"<name>\",\"actions\":[{\"channel\":\"webhook\"}]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sensing/routes', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"name\": \"<name>\",\n    \"actions\": [\n      {\n        \"channel\": \"webhook\"\n      }\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/sensing/routes\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"name\": \"<name>\",\n        \"actions\": [\n            {\n                \"channel\": \"webhook\",\n            },\n        ],\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sensing/routes');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'name' => '<name>',\n        'actions' => [\n            [\n                'channel' => 'webhook',\n            ],\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/sensing/routes/{routeId}": {
      "delete": {
        "operationId": "deletePartnerSensingRoute",
        "parameters": [
          {
            "in": "path",
            "name": "routeId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "routeId": "rt_8f21c0a94b6d47e3"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "routeId": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The route was soft-deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete (soft) a Sense Anything Route",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X DELETE \"https://api.trillboards.com/v1/partner/sensing/routes/<routeId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sensing/routes/<routeId>', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.delete(\n    \"https://api.trillboards.com/v1/partner/sensing/routes/<routeId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sensing/routes/<routeId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "get": {
        "operationId": "getPartnerSensingRoute",
        "parameters": [
          {
            "in": "path",
            "name": "routeId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "route": {
                      "actions": [
                        {
                          "actionId": "ra_2c7e40b1d9f8",
                          "attachClip": true,
                          "channel": "whatsapp",
                          "clipDurationSeconds": 30,
                          "enabled": true,
                          "recipientE164": "+12025550123",
                          "recipientEmail": null,
                          "routeId": "rt_8f21c0a94b6d47e3",
                          "slackChannelId": null,
                          "templateName": "tb_alert_threshold",
                          "webhookId": null
                        }
                      ],
                      "activeDaysMask": 127,
                      "activeEndMinute": 360,
                      "activeStartMinute": 1200,
                      "activeTz": "America/New_York",
                      "coalesceWindowSeconds": 0,
                      "condition": {
                        "field": "loiter_count",
                        "op": "gt",
                        "value": 0
                      },
                      "confirmSamples": null,
                      "confirmWindowSeconds": null,
                      "createdAt": "2026-08-30T14:11:58.204Z",
                      "createdBy": "partner:6507a1b2c3d4e5f6a7b8cc33",
                      "debounceSeconds": 300,
                      "deletedAt": null,
                      "digest": null,
                      "enabled": true,
                      "entityMemorySeconds": null,
                      "entityMinDwellSeconds": null,
                      "escalationStepNum": null,
                      "forceRealtime": false,
                      "mode": "threshold",
                      "nagIntervalSeconds": null,
                      "name": "Loitering after 8pm",
                      "ownerId": "6507a1b2c3d4e5f6a7b8cc33",
                      "ownerKind": "partner",
                      "partnerId": "6507a1b2c3d4e5f6a7b8cc33",
                      "profileName": "parking_watch",
                      "reFirePolicy": "once_per_episode",
                      "routeId": "rt_8f21c0a94b6d47e3",
                      "screenFocusRegions": [
                        {
                          "height": 0.6,
                          "screenMongoId": "507f1f77bcf86cd799439011",
                          "width": 0.5,
                          "x": 0.1,
                          "y": 0.2
                        }
                      ],
                      "screenMongoIds": [
                        "507f1f77bcf86cd799439011"
                      ],
                      "sustainMinSamples": 1,
                      "sustainSeconds": null,
                      "sustainWindowSeconds": 120,
                      "targetGroupId": null,
                      "updatedAt": "2026-08-30T14:12:07.480Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "route": {
                          "$ref": "#/components/schemas/SensingRoute"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The route"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get one Sense Anything Route",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/sensing/routes/<routeId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sensing/routes/<routeId>', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/sensing/routes/<routeId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sensing/routes/<routeId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Update only the fields present in the body; omitted fields are left\nunchanged. Re-targeting `screenMongoIds` re-authorizes the new screen set\nagainst the partner's owned screens (403 on a foreign screen).\n",
        "operationId": "updatePartnerSensingRoute",
        "parameters": [
          {
            "in": "path",
            "name": "routeId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdatePartnerSensingRouteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "route": {
                      "actions": [
                        {
                          "actionId": "ra_2c7e40b1d9f8",
                          "attachClip": true,
                          "channel": "whatsapp",
                          "clipDurationSeconds": 30,
                          "enabled": true,
                          "recipientE164": "+12025550123",
                          "recipientEmail": null,
                          "routeId": "rt_8f21c0a94b6d47e3",
                          "slackChannelId": null,
                          "templateName": "tb_alert_threshold",
                          "webhookId": null
                        }
                      ],
                      "activeDaysMask": 127,
                      "activeEndMinute": 360,
                      "activeStartMinute": 1200,
                      "activeTz": "America/New_York",
                      "coalesceWindowSeconds": 0,
                      "condition": {
                        "field": "loiter_count",
                        "op": "gt",
                        "value": 0
                      },
                      "confirmSamples": null,
                      "confirmWindowSeconds": null,
                      "createdAt": "2026-08-30T14:11:58.204Z",
                      "createdBy": "partner:6507a1b2c3d4e5f6a7b8cc33",
                      "debounceSeconds": 300,
                      "deletedAt": null,
                      "digest": null,
                      "enabled": false,
                      "entityMemorySeconds": null,
                      "entityMinDwellSeconds": null,
                      "escalationStepNum": null,
                      "forceRealtime": false,
                      "mode": "threshold",
                      "nagIntervalSeconds": null,
                      "name": "Loitering after 8pm",
                      "ownerId": "6507a1b2c3d4e5f6a7b8cc33",
                      "ownerKind": "partner",
                      "partnerId": "6507a1b2c3d4e5f6a7b8cc33",
                      "profileName": "parking_watch",
                      "reFirePolicy": "once_per_episode",
                      "routeId": "rt_8f21c0a94b6d47e3",
                      "screenFocusRegions": [
                        {
                          "height": 0.6,
                          "screenMongoId": "507f1f77bcf86cd799439011",
                          "width": 0.5,
                          "x": 0.1,
                          "y": 0.2
                        }
                      ],
                      "screenMongoIds": [
                        "507f1f77bcf86cd799439011"
                      ],
                      "sustainMinSamples": 1,
                      "sustainSeconds": null,
                      "sustainWindowSeconds": 120,
                      "targetGroupId": null,
                      "updatedAt": "2026-08-31T09:04:22.115Z"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "route": {
                          "$ref": "#/components/schemas/SensingRoute"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The updated route"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "One or more screens are not owned by this partner"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update a Sense Anything Route (partial, non-clobbering)",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/sensing/routes/<routeId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"reviewTimingMode\":\"immediate\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/sensing/routes/<routeId>', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"reviewTimingMode\": \"immediate\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/sensing/routes/<routeId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"reviewTimingMode\": \"immediate\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/sensing/routes/<routeId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'reviewTimingMode' => 'immediate',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/stripe/connect": {
      "post": {
        "description": "Creates a Stripe Connect Express account for the partner. If the partner already\nhas a Stripe account, returns the existing account details with HTTP 200.\n",
        "operationId": "createStripeConnectAccount",
        "responses": {
          "200": {
            "description": "Stripe account already exists"
          },
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "account_id": "acct_1EXAMPLE0000000",
                    "charges_enabled": false,
                    "country": "US",
                    "default_currency": "usd",
                    "details_submitted": false,
                    "next_step": "Complete onboarding at GET /v1/partner/stripe/onboarding-link",
                    "payouts_enabled": false
                  },
                  "message": "Stripe Connect account created. Complete onboarding to enable payouts.",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "account_id": {
                          "example": "acct_1234567890",
                          "type": "string"
                        },
                        "charges_enabled": {
                          "type": "boolean"
                        },
                        "country": {
                          "example": "US",
                          "type": "string"
                        },
                        "default_currency": {
                          "example": "usd",
                          "type": "string"
                        },
                        "details_submitted": {
                          "type": "boolean"
                        },
                        "next_step": {
                          "nullable": true,
                          "type": "string"
                        },
                        "payouts_enabled": {
                          "type": "boolean"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Stripe Connect account created"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Stripe API error"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create Stripe Connect account",
        "tags": [
          "Stripe Connect"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/stripe/connect\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/stripe/connect', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/stripe/connect\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/stripe/connect');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/stripe/login-link": {
      "get": {
        "description": "Returns a temporary link to the partner's Stripe Express dashboard.",
        "operationId": "getStripeLoginLink",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "account_id": "acct_1EXAMPLE0000000",
                    "instructions": "This link provides access to the Stripe Express dashboard for managing payouts.",
                    "login_url": "https://connect.stripe.com/express/EXAMPLEloginTOKEN"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "account_id": {
                          "type": "string"
                        },
                        "instructions": {
                          "type": "string"
                        },
                        "login_url": {
                          "format": "uri",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Login link generated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Partner has no Stripe account"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get Stripe dashboard login link",
        "tags": [
          "Stripe Connect"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/stripe/login-link\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/stripe/login-link', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/stripe/login-link\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/stripe/login-link');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/stripe/onboarding-link": {
      "get": {
        "description": "Returns a temporary Stripe-hosted onboarding URL where the partner can\ncomplete identity verification and bank account setup.\n",
        "operationId": "getStripeOnboardingLink",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "account_id": "acct_1EXAMPLE0000000",
                    "expires_at": "2026-09-01T14:27:31.000Z",
                    "instructions": "Redirect the partner to this URL to complete Stripe onboarding. The link expires shortly.",
                    "onboarding_url": "https://connect.stripe.com/setup/e/acct_1EXAMPLE0000000/EXAMPLEonboardingTOKEN"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "account_id": {
                          "type": "string"
                        },
                        "expires_at": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "instructions": {
                          "type": "string"
                        },
                        "onboarding_url": {
                          "format": "uri",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Onboarding link generated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Partner has no Stripe account — call POST /stripe/connect first"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get Stripe onboarding link",
        "tags": [
          "Stripe Connect"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/stripe/onboarding-link\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/stripe/onboarding-link', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/stripe/onboarding-link\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/stripe/onboarding-link');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/stripe/payout": {
      "post": {
        "description": "Initiates a payout transfer to the partner's Stripe Connect account.\nMinimum payout is $5.00 (500 cents). The partner must have an active\nStripe account and sufficient available balance.\n",
        "operationId": "requestStripePayout",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "amount_cents": {
                    "description": "Payout amount in cents (minimum $5.00)",
                    "example": 10000,
                    "minimum": 500,
                    "type": "integer"
                  },
                  "description": {
                    "description": "Optional description for the payout",
                    "type": "string"
                  },
                  "idempotency_key": {
                    "description": "Deduplication key to prevent duplicate payouts",
                    "type": "string"
                  }
                },
                "required": [
                  "amount_cents"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "amount_cents": 25000,
                    "created": "2026-09-01T14:22:31.000Z",
                    "currency": "usd",
                    "description": "Trillboards partner payout for Northwind Retail Media",
                    "destination": "acct_1EXAMPLE0000000",
                    "estimated_arrival": "2026-09-04",
                    "status": "pending",
                    "transfer_id": "tr_1EXAMPLE000000000000001"
                  },
                  "message": "Payout of $250.00 initiated",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "amount_cents": {
                          "type": "integer"
                        },
                        "created": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "currency": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        },
                        "destination": {
                          "type": "string"
                        },
                        "estimated_arrival": {
                          "description": "Estimated arrival date (2-3 business days)",
                          "format": "date",
                          "type": "string"
                        },
                        "status": {
                          "enum": [
                            "pending",
                            "reversed"
                          ],
                          "type": "string"
                        },
                        "transfer_id": {
                          "example": "tr_1234567890",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "example": "Payout of $100.00 initiated",
                      "type": "string"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Payout initiated"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Invalid amount, insufficient balance, or account not active"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Request a payout",
        "tags": [
          "Stripe Connect"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/stripe/payout\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"amount_cents\":10000}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/stripe/payout', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"amount_cents\": 10000\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/stripe/payout\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"amount_cents\": 10000,\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/stripe/payout');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'amount_cents' => 10000,\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/stripe/payouts": {
      "get": {
        "description": "Returns a paginated list of past Stripe payouts for the partner.",
        "operationId": "getStripePayoutHistory",
        "parameters": [
          {
            "description": "Maximum number of payouts to return",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 25,
              "maximum": 100,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "has_more": false,
                    "total": 2,
                    "transfers": [
                      {
                        "amount_cents": 25000,
                        "created": "2026-09-01T14:22:31.000Z",
                        "currency": "usd",
                        "description": "Trillboards partner payout for Northwind Retail Media",
                        "status": "completed",
                        "transfer_id": "tr_1EXAMPLE000000000000001"
                      },
                      {
                        "amount_cents": 18750,
                        "created": "2026-08-01T14:05:02.000Z",
                        "currency": "usd",
                        "description": "Trillboards partner payout for Northwind Retail Media",
                        "status": "completed",
                        "transfer_id": "tr_1EXAMPLE000000000000000"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "has_more": {
                          "type": "boolean"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "transfers": {
                          "items": {
                            "properties": {
                              "amount_cents": {
                                "type": "integer"
                              },
                              "created": {
                                "format": "date-time",
                                "type": "string"
                              },
                              "currency": {
                                "type": "string"
                              },
                              "description": {
                                "type": "string"
                              },
                              "status": {
                                "enum": [
                                  "completed",
                                  "reversed"
                                ],
                                "type": "string"
                              },
                              "transfer_id": {
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Payout history"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get payout history",
        "tags": [
          "Stripe Connect"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/stripe/payouts\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/stripe/payouts', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/stripe/payouts\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/stripe/payouts');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/stripe/status": {
      "get": {
        "description": "Returns the current status of the partner's Stripe Connect account including\nonboarding requirements and payout readiness.\n",
        "operationId": "getStripeAccountStatus",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "account_id": "acct_1EXAMPLE0000000",
                    "charges_enabled": true,
                    "country": "US",
                    "default_currency": "usd",
                    "details_submitted": true,
                    "has_account": true,
                    "payouts_enabled": true,
                    "requirements": {
                      "currently_due": [],
                      "disabled_reason": null,
                      "eventually_due": [
                        "company.tax_id"
                      ],
                      "past_due": []
                    },
                    "status": "active"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "account_id": {
                          "nullable": true,
                          "type": "string"
                        },
                        "charges_enabled": {
                          "type": "boolean"
                        },
                        "country": {
                          "type": "string"
                        },
                        "default_currency": {
                          "type": "string"
                        },
                        "details_submitted": {
                          "type": "boolean"
                        },
                        "has_account": {
                          "type": "boolean"
                        },
                        "payouts_enabled": {
                          "type": "boolean"
                        },
                        "requirements": {
                          "properties": {
                            "currently_due": {
                              "items": {
                                "type": "string"
                              },
                              "type": "array"
                            },
                            "disabled_reason": {
                              "nullable": true,
                              "type": "string"
                            },
                            "eventually_due": {
                              "items": {
                                "type": "string"
                              },
                              "type": "array"
                            },
                            "past_due": {
                              "items": {
                                "type": "string"
                              },
                              "type": "array"
                            }
                          },
                          "type": "object"
                        },
                        "status": {
                          "enum": [
                            "not_connected",
                            "pending",
                            "active",
                            "restricted"
                          ],
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Account status"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get Stripe account status",
        "tags": [
          "Stripe Connect"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/stripe/status\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/stripe/status', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/stripe/status\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/stripe/status');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/stripe/webhook": {
      "post": {
        "description": "Receives webhook events from Stripe for Connect account updates and transfer status changes.\nThis endpoint is called by Stripe, not by the partner. It uses Stripe signature verification.\n\n**Handled events:** `account.updated`, `transfer.created`, `transfer.paid`, `transfer.reversed`\n",
        "operationId": "handleStripeWebhook",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "description": "Raw Stripe webhook event payload",
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "event_type": "account.updated",
                  "new_status": "active",
                  "old_status": "pending",
                  "partner_id": "6a07331f0c9d42ab15e77d20",
                  "partner_slug": "northwind-retail",
                  "processed": true,
                  "received": true
                },
                "schema": {
                  "properties": {
                    "event_type": {
                      "type": "string"
                    },
                    "received": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Webhook acknowledged"
          }
        },
        "security": [],
        "summary": "Stripe webhook receiver",
        "tags": [
          "Stripe Connect"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/stripe/webhook\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/stripe/webhook', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({}),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/stripe/webhook\",\n    json={},\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/stripe/webhook');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/team": {
      "get": {
        "description": "List all team members for the partner organization.\nReturns member info, roles, and permissions.\n",
        "operationId": "listTeamMembers",
        "parameters": [
          {
            "description": "Filter by membership status",
            "in": "query",
            "name": "status",
            "schema": {
              "default": "active",
              "enum": [
                "active",
                "pending",
                "all"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "members": [
                      {
                        "accepted_at": "2025-12-01T00:00:00.000Z",
                        "email": "owner@example.com",
                        "invited_at": "2025-12-01T00:00:00.000Z",
                        "membership_id": "507f1f77bcf86cd799439011",
                        "name": "Jane Owner",
                        "permissions": {
                          "manage_team": true,
                          "view_analytics": true,
                          "view_earnings": true,
                          "view_screen_status": true,
                          "view_screens": true
                        },
                        "photo": null,
                        "role": "owner",
                        "status": "active",
                        "user_id": "507f1f77bcf86cd799439012"
                      },
                      {
                        "accepted_at": "2026-01-15T12:00:00.000Z",
                        "email": "viewer@example.com",
                        "invited_at": "2026-01-15T00:00:00.000Z",
                        "membership_id": "507f1f77bcf86cd799439013",
                        "name": "John Viewer",
                        "permissions": {
                          "manage_team": false,
                          "view_analytics": false,
                          "view_earnings": false,
                          "view_screen_status": true,
                          "view_screens": true
                        },
                        "photo": null,
                        "role": "viewer",
                        "status": "active",
                        "user_id": "507f1f77bcf86cd799439014"
                      }
                    ],
                    "partner": {
                      "name": "Acme Vending Co",
                      "slug": "acmevending"
                    },
                    "total": 2
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "members": {
                          "items": {
                            "$ref": "#/components/schemas/TeamMember"
                          },
                          "type": "array"
                        },
                        "partner": {
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "slug": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "total": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Team members retrieved"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List team members",
        "tags": [
          "Team"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/team\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/team', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/team\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/team');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/team/accept": {
      "post": {
        "description": "Accept a team invitation using the token from the invitation email.\nCan be called with or without authentication.\n\n**Without auth:** User with the invited email must exist\n**With auth:** Links invitation to the authenticated user's account\n",
        "operationId": "acceptInvitation",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "token": "inv_abc123def456"
              },
              "schema": {
                "properties": {
                  "token": {
                    "description": "Invitation token from email link",
                    "type": "string"
                  }
                },
                "required": [
                  "token"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "partner": {
                      "id": "507f1f77bcf86cd799439011",
                      "name": "Acme Vending Co",
                      "slug": "acmevending"
                    },
                    "permissions": {
                      "view_analytics": false,
                      "view_screen_status": true,
                      "view_screens": true
                    },
                    "portal_url": "https://trillboards.com/earner",
                    "role": "viewer"
                  },
                  "message": "Successfully joined Acme Vending Co",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "partner": {
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "slug": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "permissions": {
                          "$ref": "#/components/schemas/RolePermissions"
                        },
                        "portal_url": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Invitation accepted"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Token missing or user account required"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Invalid or expired invitation token"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Already a member of this organization"
          }
        },
        "security": [
          {},
          {
            "BearerAuth": []
          }
        ],
        "summary": "Accept team invitation",
        "tags": [
          "Team"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/team/accept\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"token\":\"inv_abc123def456\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/team/accept', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"token\": \"inv_abc123def456\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/team/accept\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"token\": \"inv_abc123def456\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/team/accept');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'token' => 'inv_abc123def456',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/team/invite": {
      "post": {
        "description": "Invite a new team member to join your partner organization.\nThe invitee will receive an email with instructions to join.\n\n**Authorization:** Requires both a Partner API key and a portal user JWT\nfor an owner/admin in `X-User-Token`. API keys alone cannot invite users;\nteam management is role-based so access changes are tied to a signed-in\nhuman owner or admin. Portal users can do the same action in\n**Settings → Team** after claiming the owner account created during\nregistration.\n\n**Roles:**\n- `admin` - Can manage screens, view analytics, configure webhooks\n- `viewer` - Can only view screen list and status\n",
        "operationId": "inviteTeamMember",
        "parameters": [
          {
            "description": "User JWT token for authorization",
            "in": "header",
            "name": "X-User-Token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "email": "teammate@example.com",
                "name": "John Smith",
                "role": "viewer"
              },
              "schema": {
                "properties": {
                  "email": {
                    "description": "Email address of the invitee",
                    "format": "email",
                    "type": "string"
                  },
                  "name": {
                    "description": "Optional name of the invitee",
                    "type": "string"
                  },
                  "role": {
                    "default": "viewer",
                    "description": "Role to assign to the team member",
                    "enum": [
                      "admin",
                      "viewer"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "email"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "email": "teammate@example.com",
                    "expires_at": "2026-02-02T00:00:00.000Z",
                    "membership_id": "507f1f77bcf86cd799439011",
                    "role": "viewer",
                    "status": "pending"
                  },
                  "message": "Invitation sent successfully",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TeamInviteResponse"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Invitation sent"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Only owners can invite team members"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "User already has membership or pending invitation"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Invite a team member",
        "tags": [
          "Team"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/team/invite\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"X-User-Token: <X-User-Token>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\":\"teammate@example.com\",\"role\":\"viewer\",\"name\":\"John Smith\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/team/invite', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'X-User-Token': \"<X-User-Token>\",\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"email\": \"teammate@example.com\",\n    \"role\": \"viewer\",\n    \"name\": \"John Smith\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/team/invite\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n        \"X-User-Token\": \"<X-User-Token>\",\n    },\n    json={\n        \"email\": \"teammate@example.com\",\n        \"role\": \"viewer\",\n        \"name\": \"John Smith\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/team/invite');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'X-User-Token: <X-User-Token>',\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'email' => 'teammate@example.com',\n        'role' => 'viewer',\n        'name' => 'John Smith',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/team/invite/{token}": {
      "get": {
        "description": "Get details about an invitation by token.\nThis is a public endpoint used during the signup flow.\n",
        "operationId": "getInvitationByToken",
        "parameters": [
          {
            "description": "Invitation token from email link",
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "email": "teammate@example.com",
                    "expires_at": "2026-02-02T00:00:00.000Z",
                    "invited_at": "2026-01-26T00:00:00.000Z",
                    "partner": {
                      "name": "Acme Vending Co",
                      "slug": "acmevending"
                    },
                    "role": "viewer"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "email": {
                          "type": "string"
                        },
                        "expires_at": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "invited_at": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "partner": {
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "slug": {
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "role": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Invitation details retrieved"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Invalid or expired invitation"
          }
        },
        "security": [],
        "summary": "Get invitation details",
        "tags": [
          "Team"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/team/invite/<token>\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/team/invite/<token>');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/team/invite/<token>\",\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/team/invite/<token>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/team/{membershipId}/resend": {
      "post": {
        "description": "Resend the invitation email to a pending team member.\nGenerates a new invitation token with extended expiry.\n\n**Authorization:** Requires both partner API key AND user JWT with owner role.\n",
        "operationId": "resendInvitation",
        "parameters": [
          {
            "description": "Membership ID of the pending invitation",
            "in": "path",
            "name": "membershipId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "User JWT token for authorization",
            "in": "header",
            "name": "X-User-Token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "email": "teammate@example.com",
                    "expires_at": "2026-02-02T00:00:00.000Z"
                  },
                  "message": "Invitation resent successfully",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "email": {
                          "type": "string"
                        },
                        "expires_at": {
                          "format": "date-time",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Invitation resent"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Only owners can resend invitations"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Pending invitation not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Resend invitation",
        "tags": [
          "Team"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/team/<membershipId>/resend\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"X-User-Token: <X-User-Token>\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/team/<membershipId>/resend', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'X-User-Token': \"<X-User-Token>\",\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/team/<membershipId>/resend\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n        \"X-User-Token\": \"<X-User-Token>\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/team/<membershipId>/resend');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'X-User-Token: <X-User-Token>',\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/team/{userId}": {
      "delete": {
        "description": "Remove a team member from the partner organization.\nThe owner cannot be removed.\n\n**Authorization:** Requires both partner API key AND user JWT with owner role.\n",
        "operationId": "removeTeamMember",
        "parameters": [
          {
            "description": "User ID, membership ID, or email of the member to remove",
            "in": "path",
            "name": "userId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "User JWT token for authorization",
            "in": "header",
            "name": "X-User-Token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "message": "Team member removed successfully",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "message": {
                      "example": "Team member removed successfully",
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Team member removed"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Cannot remove owner"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Only owners can remove team members"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Remove a team member",
        "tags": [
          "Team"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X DELETE \"https://api.trillboards.com/v1/partner/team/<userId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"X-User-Token: <X-User-Token>\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/team/<userId>', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'X-User-Token': \"<X-User-Token>\",\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.delete(\n    \"https://api.trillboards.com/v1/partner/team/<userId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n        \"X-User-Token\": \"<X-User-Token>\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/team/<userId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'X-User-Token: <X-User-Token>',\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/team/{userId}/role": {
      "patch": {
        "description": "Change the role of a team member. Cannot change owner's role.\n\n**Authorization:** Requires both partner API key AND user JWT with owner role.\n",
        "operationId": "changeTeamMemberRole",
        "parameters": [
          {
            "description": "User ID of the member",
            "in": "path",
            "name": "userId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "User JWT token for authorization",
            "in": "header",
            "name": "X-User-Token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "role": "admin"
              },
              "schema": {
                "properties": {
                  "role": {
                    "enum": [
                      "admin",
                      "viewer"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "role"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "new_role": "admin",
                    "old_role": "viewer",
                    "permissions": {
                      "edit_screens": true,
                      "manage_team": false,
                      "view_analytics": true,
                      "view_earnings": false,
                      "view_impressions": true,
                      "view_screen_status": true,
                      "view_screens": true
                    },
                    "user_id": "507f1f77bcf86cd799439014"
                  },
                  "message": "Role changed from viewer to admin",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "new_role": {
                          "type": "string"
                        },
                        "old_role": {
                          "type": "string"
                        },
                        "permissions": {
                          "$ref": "#/components/schemas/RolePermissions"
                        },
                        "user_id": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Role changed"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Cannot change owner's role or invalid role"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Only owners can change roles"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Change team member role",
        "tags": [
          "Team"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/team/<userId>/role\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"X-User-Token: <X-User-Token>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"role\":\"admin\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/team/<userId>/role', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'X-User-Token': \"<X-User-Token>\",\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"role\": \"admin\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/team/<userId>/role\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n        \"X-User-Token\": \"<X-User-Token>\",\n    },\n    json={\n        \"role\": \"admin\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/team/<userId>/role');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'X-User-Token: <X-User-Token>',\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'role' => 'admin',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/tracking/.well-known/public-key": {
      "get": {
        "description": "Returns the Ed25519 public key used to sign impression proofs.\n\n**No authentication required.** This is a public endpoint to enable third-party verification.\n\n**Verification Process:**\n1. Fetch public key from this endpoint\n2. Reconstruct canonical payload: `v2.{timestamp}.{adId}.{impressionId}.{screenId}.{deviceId}`\n3. Verify Ed25519 signature using the public key\n\nSee `/tracking/batch` response for signature format.\n",
        "operationId": "getTrackingPublicKey",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "algorithm": "ed25519",
                    "documentation": "https://api.trillboards.com/docs/proof-of-play",
                    "keyId": "pk_v2_001",
                    "publicKey": "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
                    "verificationUrl": "https://api.trillboards.com/v1/advertiser/proof/verify-proof",
                    "version": "v2"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "algorithm": {
                          "description": "Signature algorithm (ed25519 for public key, sha256 for HMAC fallback)",
                          "enum": [
                            "ed25519",
                            "sha256"
                          ],
                          "example": "ed25519",
                          "type": "string"
                        },
                        "documentation": {
                          "example": "https://api.trillboards.com/docs/proof-of-play",
                          "type": "string"
                        },
                        "keyId": {
                          "description": "Key identifier for rotation support",
                          "example": "pk_v2_001",
                          "type": "string"
                        },
                        "publicKey": {
                          "description": "Hex-encoded Ed25519 public key (null if using HMAC fallback)",
                          "example": "7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b",
                          "nullable": true,
                          "type": "string"
                        },
                        "verificationUrl": {
                          "example": "https://api.trillboards.com/v1/advertiser/proof/verify-proof",
                          "type": "string"
                        },
                        "version": {
                          "example": "v2",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Public key information",
            "headers": {
              "Cache-Control": {
                "description": "Caching directive (24 hours)",
                "schema": {
                  "example": "public, max-age=86400",
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [],
        "summary": "Get public key for proof verification",
        "tags": [
          "Batch Tracking"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/tracking/.well-known/public-key\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/tracking/.well-known/public-key');\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/tracking/.well-known/public-key\",\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/tracking/.well-known/public-key');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/tracking/batch": {
      "post": {
        "description": "High-volume endpoint for reporting multiple impressions in a single request.\n\n**Features:**\n- Process up to 100 impressions per batch\n- Ed25519 cryptographic proof for each impression\n- Duplicate detection (impressions with same impression_id + ad_id are ignored)\n- Optional audience metrics (face count, attention)\n\n**Batching Strategy:**\n- Buffer impressions client-side\n- Flush every 1-5 minutes or when buffer reaches 100\n- Handle failures with local retry queue\n\n**Proof Verification:**\nEach proof can be independently verified using the public key from `/tracking/.well-known/public-key`.\n",
        "operationId": "recordTrackingBatch",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "impressions": [
                  {
                    "ad_id": "ima_1706745600_abc123",
                    "audience": {
                      "attention": 0.87,
                      "face_count": 3
                    },
                    "device_id": "PARTNER-DEVICE-001",
                    "duration_ms": 15000,
                    "event": "complete",
                    "timestamp": "2026-01-31T10:00:00Z"
                  },
                  {
                    "ad_id": "ima_1706745600_def456",
                    "device_id": "PARTNER-DEVICE-002",
                    "duration_ms": 30000,
                    "event": "complete",
                    "timestamp": "2026-01-31T10:00:05Z"
                  }
                ]
              },
              "schema": {
                "properties": {
                  "impressions": {
                    "items": {
                      "properties": {
                        "ad_id": {
                          "description": "Ad identifier from VAST response",
                          "example": "ima_1706745600_abc123",
                          "type": "string"
                        },
                        "audience": {
                          "description": "Optional audience metrics",
                          "properties": {
                            "attention": {
                              "description": "Attention score (0-1)",
                              "example": 0.87,
                              "format": "float",
                              "maximum": 1,
                              "minimum": 0,
                              "type": "number"
                            },
                            "dwell_time_ms": {
                              "description": "How long viewers were present",
                              "example": 45000,
                              "type": "integer"
                            },
                            "face_count": {
                              "description": "Number of viewers detected",
                              "example": 3,
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "device_id": {
                          "description": "Partner's device identifier",
                          "example": "PARTNER-DEVICE-001",
                          "type": "string"
                        },
                        "duration_ms": {
                          "description": "Video duration played in milliseconds",
                          "example": 15000,
                          "type": "integer"
                        },
                        "event": {
                          "default": "complete",
                          "description": "VAST event type",
                          "enum": [
                            "start",
                            "complete",
                            "error",
                            "firstQuartile",
                            "midpoint",
                            "thirdQuartile"
                          ],
                          "type": "string"
                        },
                        "impression_id": {
                          "description": "Optional client-generated ID (auto-generated if not provided)",
                          "example": "imp_client_abc123",
                          "type": "string"
                        },
                        "timestamp": {
                          "description": "When the event occurred (defaults to now)",
                          "example": "2026-01-31T10:00:00Z",
                          "format": "date-time",
                          "type": "string"
                        }
                      },
                      "required": [
                        "device_id",
                        "ad_id"
                      ],
                      "type": "object"
                    },
                    "maxItems": 100,
                    "type": "array"
                  }
                },
                "required": [
                  "impressions"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "duplicates": 0,
                    "errors": [],
                    "failed": 0,
                    "processed": 2,
                    "processing_time_ms": 45,
                    "proofs": [
                      {
                        "impression_id": "imp_1706745600_a1b2c3",
                        "public_key_id": "pk_v2_001",
                        "signature": "ed25519=7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
                      },
                      {
                        "impression_id": "imp_1706745605_d4e5f6",
                        "public_key_id": "pk_v2_001",
                        "signature": "ed25519=8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "duplicates": {
                          "description": "Number of duplicate impressions skipped",
                          "example": 0,
                          "type": "integer"
                        },
                        "errors": {
                          "description": "Details of failed impressions (max 10 shown)",
                          "items": {
                            "properties": {
                              "error": {
                                "type": "string"
                              },
                              "impression_id": {
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "failed": {
                          "description": "Number of impressions that failed validation",
                          "example": 0,
                          "type": "integer"
                        },
                        "processed": {
                          "description": "Number of impressions successfully recorded",
                          "example": 100,
                          "type": "integer"
                        },
                        "processing_time_ms": {
                          "description": "Time to process the batch",
                          "example": 45,
                          "type": "integer"
                        },
                        "proofs": {
                          "description": "Cryptographic proofs for each processed impression",
                          "items": {
                            "properties": {
                              "impression_id": {
                                "example": "imp_1706745600_a1b2c3",
                                "type": "string"
                              },
                              "public_key_id": {
                                "example": "pk_v2_001",
                                "type": "string"
                              },
                              "signature": {
                                "description": "Ed25519 signature (or SHA256 fallback)",
                                "example": "ed25519=7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d...",
                                "type": "string"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Batch processed successfully"
          },
          "400": {
            "content": {
              "application/json": {
                "examples": {
                  "empty_array": {
                    "value": {
                      "message": "impressions array cannot be empty",
                      "success": false
                    }
                  },
                  "too_many": {
                    "value": {
                      "message": "Maximum 100 impressions per batch",
                      "success": false
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "Invalid request (empty array, too many impressions, etc.)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Record batch impressions with proof generation",
        "tags": [
          "Batch Tracking"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/tracking/batch\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"impressions\":[{\"device_id\":\"PARTNER-DEVICE-001\",\"ad_id\":\"ima_1706745600_abc123\",\"event\":\"complete\",\"timestamp\":\"2026-01-31T10:00:00Z\",\"duration_ms\":15000,\"audience\":{\"face_count\":3,\"attention\":0.87}},{\"device_id\":\"PARTNER-DEVICE-002\",\"ad_id\":\"ima_1706745600_def456\",\"event\":\"complete\",\"timestamp\":\"2026-01-31T10:00:05Z\",\"duration_ms\":30000}]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/tracking/batch', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"impressions\": [\n      {\n        \"device_id\": \"PARTNER-DEVICE-001\",\n        \"ad_id\": \"ima_1706745600_abc123\",\n        \"event\": \"complete\",\n        \"timestamp\": \"2026-01-31T10:00:00Z\",\n        \"duration_ms\": 15000,\n        \"audience\": {\n          \"face_count\": 3,\n          \"attention\": 0.87\n        }\n      },\n      {\n        \"device_id\": \"PARTNER-DEVICE-002\",\n        \"ad_id\": \"ima_1706745600_def456\",\n        \"event\": \"complete\",\n        \"timestamp\": \"2026-01-31T10:00:05Z\",\n        \"duration_ms\": 30000\n      }\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/tracking/batch\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"impressions\": [\n            {\n                \"device_id\": \"PARTNER-DEVICE-001\",\n                \"ad_id\": \"ima_1706745600_abc123\",\n                \"event\": \"complete\",\n                \"timestamp\": \"2026-01-31T10:00:00Z\",\n                \"duration_ms\": 15000,\n                \"audience\": {\n                    \"face_count\": 3,\n                    \"attention\": 0.87,\n                },\n            },\n            {\n                \"device_id\": \"PARTNER-DEVICE-002\",\n                \"ad_id\": \"ima_1706745600_def456\",\n                \"event\": \"complete\",\n                \"timestamp\": \"2026-01-31T10:00:05Z\",\n                \"duration_ms\": 30000,\n            },\n        ],\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/tracking/batch');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'impressions' => [\n            [\n                'device_id' => 'PARTNER-DEVICE-001',\n                'ad_id' => 'ima_1706745600_abc123',\n                'event' => 'complete',\n                'timestamp' => '2026-01-31T10:00:00Z',\n                'duration_ms' => 15000,\n                'audience' => [\n                    'face_count' => 3,\n                    'attention' => 0.87,\n                ],\n            ],\n            [\n                'device_id' => 'PARTNER-DEVICE-002',\n                'ad_id' => 'ima_1706745600_def456',\n                'event' => 'complete',\n                'timestamp' => '2026-01-31T10:00:05Z',\n                'duration_ms' => 30000,\n            ],\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/usage": {
      "get": {
        "description": "Returns API usage statistics including total calls, error rates,\ntop endpoints, and daily breakdown. Default range is 30 days, maximum 90 days.\n",
        "operationId": "getUsageSummary",
        "parameters": [
          {
            "description": "Start date (ISO 8601). Defaults to 30 days ago.",
            "in": "query",
            "name": "start_date",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "End date (ISO 8601). Defaults to now.",
            "in": "query",
            "name": "end_date",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "avgResponseTime": 87,
                  "dailyBreakdown": [
                    {
                      "avg_response_ms": 84.6,
                      "date": "2026-08-29T00:00:00.000Z",
                      "total_calls": 6142,
                      "total_errors": 11
                    },
                    {
                      "avg_response_ms": 82.1,
                      "date": "2026-08-30T00:00:00.000Z",
                      "total_calls": 6008,
                      "total_errors": 9
                    }
                  ],
                  "errorRate": 0.22,
                  "maxResponseTime": 4231,
                  "object": "usage_summary",
                  "period": {
                    "end_date": "2026-08-31T14:22:07.512Z",
                    "start_date": "2026-08-01T14:22:07.512Z"
                  },
                  "topEndpoints": [
                    {
                      "avg_response_ms": 62.4,
                      "endpoint": "/v1/partner/devices",
                      "method": "GET",
                      "total_calls": 151884,
                      "total_errors": 96
                    },
                    {
                      "avg_response_ms": 141.2,
                      "endpoint": "/v1/partner/venues/:id/intelligence",
                      "method": "GET",
                      "total_calls": 24117,
                      "total_errors": 18
                    }
                  ],
                  "totalCalls": 184203,
                  "totalErrors": 412
                },
                "schema": {
                  "properties": {
                    "avgResponseTime": {
                      "description": "Average response time in milliseconds",
                      "type": "number"
                    },
                    "dailyBreakdown": {
                      "items": {
                        "properties": {
                          "avg_response_ms": {
                            "type": "number"
                          },
                          "date": {
                            "description": "The UTC day, sent as the timestamp of its midnight.",
                            "format": "date-time",
                            "type": "string"
                          },
                          "total_calls": {
                            "type": "integer"
                          },
                          "total_errors": {
                            "type": "integer"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "errorRate": {
                      "description": "Error rate percentage (0-100)",
                      "type": "number"
                    },
                    "object": {
                      "example": "usage_summary",
                      "type": "string"
                    },
                    "period": {
                      "properties": {
                        "end_date": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "start_date": {
                          "format": "date-time",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "topEndpoints": {
                      "items": {
                        "properties": {
                          "endpoint": {
                            "type": "string"
                          },
                          "method": {
                            "type": "string"
                          },
                          "total_calls": {
                            "type": "integer"
                          },
                          "total_errors": {
                            "type": "integer"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "totalCalls": {
                      "type": "integer"
                    },
                    "totalErrors": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Usage summary"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get API usage summary",
        "tags": [
          "Usage"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/usage\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/usage', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/usage\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/usage');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/usage/dashboard": {
      "get": {
        "description": "Returns a comprehensive usage dashboard with today's stats, rate limit status,\nand last 7 days breakdown. Combines real-time Redis data with historical MongoDB data.\n",
        "operationId": "getUsageDashboard",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "last_7_days": {
                    "avg_response_ms": 85,
                    "daily_breakdown": [
                      {
                        "avg_response_ms": 84.6,
                        "date": "2026-08-29T00:00:00.000Z",
                        "total_calls": 6142,
                        "total_errors": 11
                      },
                      {
                        "avg_response_ms": 82.1,
                        "date": "2026-08-30T00:00:00.000Z",
                        "total_calls": 6008,
                        "total_errors": 9
                      }
                    ],
                    "error_rate": 0.22,
                    "total_calls": 42918,
                    "total_errors": 94
                  },
                  "object": "usage_dashboard",
                  "rate_limit": {
                    "current_rate": 37,
                    "max_rate": 200,
                    "percent_used": 18.5,
                    "window_reset": "2026-08-31T14:23:07.512Z",
                    "window_seconds": 60
                  },
                  "today": {
                    "avg_response_ms": 79,
                    "error_rate": 0.19,
                    "top_endpoints": [
                      {
                        "avg_response_ms": 61,
                        "endpoint": "/v1/partner/devices",
                        "method": "GET",
                        "total_calls": 2611,
                        "total_errors": 3
                      },
                      {
                        "avg_response_ms": 158,
                        "endpoint": "/v1/partner/fleet/overview",
                        "method": "GET",
                        "total_calls": 573,
                        "total_errors": 3
                      }
                    ],
                    "total_calls": 3184,
                    "total_errors": 6
                  }
                },
                "schema": {
                  "properties": {
                    "last_7_days": {
                      "properties": {
                        "avg_response_ms": {
                          "type": "number"
                        },
                        "daily_breakdown": {
                          "items": {
                            "properties": {
                              "avg_response_ms": {
                                "type": "number"
                              },
                              "date": {
                                "description": "The UTC day, sent as the timestamp of its midnight.",
                                "format": "date-time",
                                "type": "string"
                              },
                              "total_calls": {
                                "type": "integer"
                              },
                              "total_errors": {
                                "type": "integer"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "error_rate": {
                          "type": "number"
                        },
                        "total_calls": {
                          "type": "integer"
                        },
                        "total_errors": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "object": {
                      "example": "usage_dashboard",
                      "type": "string"
                    },
                    "rate_limit": {
                      "properties": {
                        "current_rate": {
                          "type": "integer"
                        },
                        "max_rate": {
                          "type": "integer"
                        },
                        "percent_used": {
                          "type": "number"
                        },
                        "window_reset": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "window_seconds": {
                          "example": 60,
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "today": {
                      "properties": {
                        "avg_response_ms": {
                          "type": "number"
                        },
                        "error_rate": {
                          "type": "number"
                        },
                        "top_endpoints": {
                          "items": {
                            "properties": {
                              "avg_response_ms": {
                                "type": "number"
                              },
                              "endpoint": {
                                "type": "string"
                              },
                              "method": {
                                "type": "string"
                              },
                              "total_calls": {
                                "type": "integer"
                              },
                              "total_errors": {
                                "type": "integer"
                              }
                            },
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "total_calls": {
                          "type": "integer"
                        },
                        "total_errors": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Usage dashboard"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get real-time usage dashboard",
        "tags": [
          "Usage"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/usage/dashboard\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/usage/dashboard', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/usage/dashboard\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/usage/dashboard');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/usage/rate-limit": {
      "get": {
        "description": "Returns the partner's billing-tier rate limit metadata and their current\nobserved call rate.\n\n> **The `max_rate` in this response is billing-tier metadata, not the limit the\n> server enforces.** Enforcement for authenticated partner endpoints is a flat\n> **1000 requests/minute per API key**, applied identically to every tier. A\n> `basic` partner is therefore *not* throttled at 200/min, and an `enterprise`\n> partner does not get 5000/min. Size your client against 1000/min and treat\n> `429` + `Retry-After` as the only authoritative signal.\n\n`max_rate` is resolved from the partner's effective tier, which is derived from\nbilling status (`enterprise` when pay-per-use billing is configured, otherwise\n`basic`); the `tier` field echoes the stored `data_access_tier` separately.\n\n| Tier | Reported `max_rate` | Actually enforced |\n|------|--------------------|-------------------|\n| Basic | 200/min | 1000/min |\n| Developer | 1,000/min | 1000/min |\n| Enterprise | 5,000/min | 1000/min |\n",
        "operationId": "getRateLimitStatus",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "billing_status": "free",
                  "current_rate": 37,
                  "max_rate": 200,
                  "object": "rate_limit_status",
                  "percent_used": 18.5,
                  "tier": "developer",
                  "window_reset": "2026-08-31T14:23:07.512Z",
                  "window_seconds": 60
                },
                "schema": {
                  "properties": {
                    "current_rate": {
                      "type": "integer"
                    },
                    "max_rate": {
                      "type": "integer"
                    },
                    "object": {
                      "example": "rate_limit_status",
                      "type": "string"
                    },
                    "percent_used": {
                      "type": "number"
                    },
                    "tier": {
                      "enum": [
                        "basic",
                        "developer",
                        "enterprise"
                      ],
                      "type": "string"
                    },
                    "window_reset": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "window_seconds": {
                      "example": 60,
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Rate limit status"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get rate limit status",
        "tags": [
          "Usage"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/usage/rate-limit\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/usage/rate-limit', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/usage/rate-limit\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/usage/rate-limit');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/vast/config": {
      "get": {
        "description": "Returns a VAST tag URL template that partners use to fetch ads directly from Google Ad Manager.\n\n**This is NOT a VAST proxy.** Trillboards provides the configuration; partners fetch VAST directly from Google.\n\n**Architecture:**\n1. Partner calls this endpoint (cached for 1 hour)\n2. Partner builds VAST URL using the template\n3. Partner fetches VAST directly from Google (`pubads.g.doubleclick.net`)\n4. Partner plays video in their player\n5. Partner reports impressions via `/tracking/batch`\n\n**Scale:** At 35,000 screens with 1-hour cache, this is ~10 requests/second (trivial load).\n\n**Security:** Partners only allowlist 2 domains: `api.trillboards.com` and `pubads.g.doubleclick.net`.\n",
        "operationId": "getVastConfig",
        "parameters": [
          {
            "description": "Device identifier for targeting optimization",
            "in": "query",
            "name": "device_id",
            "schema": {
              "example": "PARTNER-DEVICE-001",
              "type": "string"
            }
          },
          {
            "description": "Screen ID for targeting (alternative to device_id)",
            "in": "query",
            "name": "screen_id",
            "schema": {
              "example": "507f1f77bcf86cd799439011",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "ad_unit": "/105549217/Trillboards_Retail_Video",
                    "blocked_categories": [
                      "IAB25-3",
                      "IAB26"
                    ],
                    "cache_ttl_seconds": 3600,
                    "floor_price_cpm": 400,
                    "integration_guide": "https://api.trillboards.com/docs/partner/vast-integration",
                    "parameters": {
                      "height": 480,
                      "max_duration": 300,
                      "min_duration": 6,
                      "venuetype": 2,
                      "width": 640
                    },
                    "timeout_ms": 12000,
                    "tracking": {
                      "batch_endpoint": "https://api.trillboards.com/v1/partner/tracking/batch",
                      "max_batch_size": 100,
                      "single_endpoint": "https://api.trillboards.com/v1/partner/impression"
                    },
                    "variant": "default_retail",
                    "vast_tag_template": "https://pubads.g.doubleclick.net/gampad/ads?iu=/105549217/Trillboards_Retail_Video&sz={width}x{height}&venuetype=2&npa=0&..."
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "ad_unit": {
                          "description": "Google Ad Manager ad unit path",
                          "example": "/105549217/Trillboards_Retail_Video",
                          "type": "string"
                        },
                        "blocked_categories": {
                          "description": "IAB content categories to block",
                          "example": [
                            "IAB25-3",
                            "IAB26"
                          ],
                          "items": {
                            "type": "string"
                          },
                          "type": "array"
                        },
                        "cache_ttl_seconds": {
                          "description": "How long to cache this config",
                          "example": 3600,
                          "type": "integer"
                        },
                        "floor_price_cpm": {
                          "description": "Floor price in cents (CPM)",
                          "example": 400,
                          "type": "integer"
                        },
                        "integration_guide": {
                          "example": "https://api.trillboards.com/docs/partner/vast-integration",
                          "type": "string"
                        },
                        "parameters": {
                          "properties": {
                            "height": {
                              "example": 480,
                              "type": "integer"
                            },
                            "max_duration": {
                              "description": "Maximum ad duration in seconds",
                              "example": 300,
                              "type": "integer"
                            },
                            "min_duration": {
                              "description": "Minimum ad duration in seconds",
                              "example": 6,
                              "type": "integer"
                            },
                            "venuetype": {
                              "description": "Venue type code for targeting",
                              "example": 2,
                              "type": "integer"
                            },
                            "width": {
                              "example": 640,
                              "type": "integer"
                            }
                          },
                          "type": "object"
                        },
                        "timeout_ms": {
                          "description": "Recommended VAST fetch timeout",
                          "example": 12000,
                          "type": "integer"
                        },
                        "tracking": {
                          "properties": {
                            "batch_endpoint": {
                              "example": "https://api.trillboards.com/v1/partner/tracking/batch",
                              "type": "string"
                            },
                            "max_batch_size": {
                              "example": 100,
                              "type": "integer"
                            },
                            "single_endpoint": {
                              "example": "https://api.trillboards.com/v1/partner/impression",
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "variant": {
                          "description": "Configuration variant name for debugging",
                          "example": "default_retail",
                          "type": "string"
                        },
                        "vast_tag_template": {
                          "description": "VAST URL template with placeholders for {width} and {height}.\nPartner replaces placeholders and appends correlator.\n",
                          "example": "https://pubads.g.doubleclick.net/gampad/ads?iu=/105549217/Trillboards_Retail_Video&sz={width}x{height}&venuetype=2&...",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "VAST configuration returned successfully",
            "headers": {
              "Cache-Control": {
                "description": "Caching directive (public, max-age=3600)",
                "schema": {
                  "example": "public, max-age=3600",
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get VAST configuration for direct Google integration",
        "tags": [
          "VAST Integration"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/vast/config\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/vast/config', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/vast/config\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/vast/config');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/venues/nearby": {
      "get": {
        "description": "Returns venues within `radius_km` of `lat`/`lng`, scoped to venues\nthe partner has access to. Used for context-aware ad targeting and\nplace-of-interest analytics.\n",
        "operationId": "getVenuesNearby",
        "parameters": [
          {
            "in": "query",
            "name": "lat",
            "required": true,
            "schema": {
              "format": "double",
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "lng",
            "required": true,
            "schema": {
              "format": "double",
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "radius_km",
            "schema": {
              "default": 1,
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "center": {
                    "lat": 40.7128,
                    "lng": -74.006
                  },
                  "radiusKm": 5,
                  "venues": [
                    {
                      "busyScore": 2.1,
                      "currentOccupancy": "5-10",
                      "currentViewers": 6.4,
                      "distance": 0.42,
                      "lastUpdated": "2026-08-31T14:19:52.318Z",
                      "mood": "relaxed",
                      "screenId": "6a1f2c4d9e8b7a0c15d3e2f4",
                      "venueCategory": "cafe",
                      "venueName": "Northwind Coffee — Front Counter"
                    },
                    {
                      "busyScore": 0,
                      "currentOccupancy": null,
                      "currentViewers": 0,
                      "distance": 1.87,
                      "lastUpdated": null,
                      "mood": null,
                      "screenId": "7b2e3d5a0f9c8b1d26e4f305",
                      "venueCategory": "gym",
                      "venueName": "Riverside Fitness — Lobby"
                    }
                  ]
                },
                "schema": {
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "venues": {
                      "items": {
                        "properties": {
                          "distance_m": {
                            "type": "number"
                          },
                          "screen_id": {
                            "type": "string"
                          },
                          "venue_type": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "List of nearby venues"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List nearby Trillboards venues",
        "tags": [
          "Venue Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/venues/nearby?lat=%3Clat%3E&lng=%3Clng%3E\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/venues/nearby?lat=%3Clat%3E&lng=%3Clng%3E', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/venues/nearby?lat=%3Clat%3E&lng=%3Clng%3E\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/venues/nearby?lat=%3Clat%3E&lng=%3Clng%3E');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/venues/{screenId}/atmosphere": {
      "get": {
        "description": "Returns aggregated atmosphere signals captured on-device at the\nscreen — ambient noise level, lighting band, motion intensity,\ndominant ambient class (cafe / lobby / outdoor / gym / retail). All\nsignals are venue-level aggregates.\n",
        "operationId": "getVenueAtmosphere",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "days",
            "schema": {
              "default": 7,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "crowdDensity": "5-10",
                  "currentViewers": 6.2,
                  "dataAge": "live",
                  "engagement": 0.68,
                  "lastUpdated": "2026-08-31T14:19:52.318Z",
                  "mood": "happy",
                  "musicPlaying": false,
                  "noiseLevel": "quiet"
                },
                "schema": {
                  "properties": {
                    "days": {
                      "type": "integer"
                    },
                    "dominant_ambience": {
                      "type": "string"
                    },
                    "lighting_band": {
                      "enum": [
                        "dark",
                        "dim",
                        "lit",
                        "bright"
                      ],
                      "type": "string"
                    },
                    "motion_intensity": {
                      "type": "number"
                    },
                    "noise_level_db": {
                      "type": "number"
                    },
                    "screen_id": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Atmosphere payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Screen not found or not accessible to this partner"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Real-world atmosphere signals for a venue",
        "tags": [
          "Venue Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/venues/<screenId>/atmosphere\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/venues/<screenId>/atmosphere', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/venues/<screenId>/atmosphere\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/venues/<screenId>/atmosphere');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/venues/{screenId}/traffic": {
      "get": {
        "description": "Returns daily and hourly foot-traffic histograms for the screen,\nderived from on-device face counts and dwell times. Includes\nrolled-up busiest-hour, average-dwell, and weekday-vs-weekend\nbreakdowns.\n",
        "operationId": "getVenueTraffic",
        "parameters": [
          {
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "days",
            "schema": {
              "default": 14,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "busyScore": 5.7,
                  "hourlyTraffic": [
                    {
                      "avgShadowEvents": 1.2,
                      "avgViewers": 2.4,
                      "hour": 8,
                      "measurements": 340,
                      "occupancy": "0-5"
                    },
                    {
                      "avgShadowEvents": 3.4,
                      "avgViewers": 6.1,
                      "hour": 12,
                      "measurements": 358,
                      "occupancy": "5-10"
                    },
                    {
                      "avgShadowEvents": 4.9,
                      "avgViewers": 8.7,
                      "hour": 18,
                      "measurements": 351,
                      "occupancy": "5-10"
                    }
                  ],
                  "periodDays": 7
                },
                "schema": {
                  "properties": {
                    "daily": {
                      "items": {
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "hourly": {
                      "items": {
                        "properties": {
                          "face_count_avg": {
                            "type": "number"
                          },
                          "hour": {
                            "type": "integer"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "screen_id": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Traffic payload"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Screen not found"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Foot-traffic profile for a venue",
        "tags": [
          "Venue Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/venues/<screenId>/traffic\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/venues/<screenId>/traffic', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/venues/<screenId>/traffic\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/venues/<screenId>/traffic');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/venues/{venueId}/intelligence": {
      "get": {
        "description": "Returns aggregated venue intelligence including multi-screen deduplication,\ntimeseries data with demographics, weather context, and audience metrics.\nMaximum date range is 30 days.\n",
        "operationId": "getVenueIntelligence",
        "parameters": [
          {
            "description": "Venue ID (MongoDB ObjectId)",
            "in": "path",
            "name": "venueId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Start date (ISO 8601)",
            "in": "query",
            "name": "from",
            "required": true,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "End date (ISO 8601)",
            "in": "query",
            "name": "to",
            "required": true,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Time bucket interval",
            "in": "query",
            "name": "interval",
            "schema": {
              "default": "1h",
              "enum": [
                "5min",
                "15min",
                "1h",
                "1d"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "dedup": {
                    "camera_count": 2,
                    "method": "lincoln_petersen",
                    "raw_total": 18,
                    "unique_total": 11
                  },
                  "from": "2026-08-30T18:00:00.000Z",
                  "interval": "1h",
                  "summary": {
                    "avg_attention": 0.65,
                    "buckets": 2,
                    "total_unique_viewers": 11
                  },
                  "timeseries": [
                    {
                      "age_distribution": {
                        "18-24": 0.25,
                        "25-34": 0.4,
                        "35-49": 0.25,
                        "50+": 0.1
                      },
                      "avg_attention": 0.62,
                      "avg_engagement": 0.67,
                      "avg_noise_level": -1,
                      "brand_mention_windows": 0,
                      "bucket": "2026-08-30T18:00:00.000Z",
                      "gender_distribution": {
                        "female": 0.48,
                        "male": 0.52
                      },
                      "income_level": null,
                      "measurements": 23,
                      "mood": "happy",
                      "peak_viewers": 10,
                      "raw_viewers": 8,
                      "screen_count": 2,
                      "viewers": 5,
                      "weather": {
                        "condition": "Partly cloudy",
                        "is_raining": false,
                        "temperature_f": 72
                      }
                    },
                    {
                      "age_distribution": {
                        "18-24": 0.25,
                        "25-34": 0.4,
                        "35-49": 0.25,
                        "50+": 0.1
                      },
                      "avg_attention": 0.67,
                      "avg_engagement": 0.69,
                      "avg_noise_level": -1,
                      "brand_mention_windows": 0,
                      "bucket": "2026-08-30T19:00:00.000Z",
                      "gender_distribution": {
                        "female": 0.48,
                        "male": 0.52
                      },
                      "income_level": null,
                      "measurements": 24,
                      "mood": "happy",
                      "peak_viewers": 12,
                      "raw_viewers": 10,
                      "screen_count": 2,
                      "viewers": 6,
                      "weather": {
                        "condition": "Partly cloudy",
                        "is_raining": false,
                        "temperature_f": 72
                      }
                    }
                  ],
                  "to": "2026-08-30T20:00:00.000Z",
                  "venue_id": "6a1f2c4d9e8b7a0c15d3e2f4"
                },
                "schema": {
                  "properties": {
                    "dedup": {
                      "properties": {
                        "camera_count": {
                          "type": "integer"
                        },
                        "method": {
                          "enum": [
                            "lincoln_petersen",
                            "single_camera"
                          ],
                          "type": "string"
                        },
                        "raw_total": {
                          "type": "integer"
                        },
                        "unique_total": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "from": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "interval": {
                      "type": "string"
                    },
                    "summary": {
                      "properties": {
                        "avg_attention": {
                          "type": "number"
                        },
                        "buckets": {
                          "type": "integer"
                        },
                        "total_unique_viewers": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "timeseries": {
                      "items": {
                        "properties": {
                          "age_distribution": {
                            "additionalProperties": {
                              "type": "number"
                            },
                            "type": "object"
                          },
                          "avg_attention": {
                            "type": "number"
                          },
                          "avg_engagement": {
                            "type": "number"
                          },
                          "avg_noise_level": {
                            "nullable": true,
                            "type": "number"
                          },
                          "brand_mention_windows": {
                            "type": "integer"
                          },
                          "bucket": {
                            "format": "date-time",
                            "type": "string"
                          },
                          "gender_distribution": {
                            "properties": {
                              "female": {
                                "type": "number"
                              },
                              "male": {
                                "type": "number"
                              }
                            },
                            "type": "object"
                          },
                          "income_level": {
                            "nullable": true,
                            "type": "string"
                          },
                          "measurements": {
                            "type": "integer"
                          },
                          "mood": {
                            "nullable": true,
                            "type": "string"
                          },
                          "peak_viewers": {
                            "type": "integer"
                          },
                          "raw_viewers": {
                            "type": "integer"
                          },
                          "screen_count": {
                            "type": "integer"
                          },
                          "viewers": {
                            "description": "Deduplicated viewer count",
                            "type": "integer"
                          },
                          "weather": {
                            "properties": {
                              "condition": {
                                "nullable": true,
                                "type": "string"
                              },
                              "is_raining": {
                                "type": "boolean"
                              },
                              "temperature_f": {
                                "nullable": true,
                                "type": "number"
                              }
                            },
                            "type": "object"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "to": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "venue_id": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Venue intelligence data"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Missing or invalid parameters (from/to required, max 30 days)"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            },
            "description": "Venue not found or not authorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get venue intelligence report",
        "tags": [
          "Venue Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/venues/<venueId>/intelligence?from=%3Cfrom%3E&to=%3Cto%3E\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/venues/<venueId>/intelligence?from=%3Cfrom%3E&to=%3Cto%3E', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/venues/<venueId>/intelligence?from=%3Cfrom%3E&to=%3Cto%3E\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/venues/<venueId>/intelligence?from=%3Cfrom%3E&to=%3Cto%3E');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/webhooks": {
      "get": {
        "description": "Get all webhook subscriptions for your account.",
        "operationId": "listWebhooks",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "webhooks": [
                      {
                        "created_at": "2025-12-01T00:00:00.000Z",
                        "events": [
                          "device.online",
                          "device.offline"
                        ],
                        "failure_count": 2,
                        "last_triggered_at": "2026-01-01T02:30:00.000Z",
                        "status": "active",
                        "success_count": 42,
                        "url": "https://example.com/webhooks/trillboards",
                        "webhook_id": "wh_m4k7x9_a1b2c3d4e5f6"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "webhooks": {
                          "items": {
                            "$ref": "#/components/schemas/Webhook"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Webhooks list"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "List webhooks",
        "tags": [
          "Webhooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/webhooks\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/webhooks', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/webhooks\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/webhooks');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "post": {
        "description": "Subscribe to webhook events. Webhooks deliver real-time notifications\nwhen events occur on your devices.\n\n**Available Events:**\n- `device.online` - Device comes online after being offline\n- `device.offline` - Device goes offline (no heartbeat for 5+ minutes)\n- `device.requirements_updated` - A screen's `monetization_status` changed (e.g. → ready when classification completes, or → blocked when it permanently fails). Payload carries the full `monetization_status` + `requirements` + `location_capability` so you can react without polling.\n- `impression.recorded` - Ad impression recorded on a device\n- `creative.rewards_ready` - Native measured play states changed for your funded creative experiment. The hint carries a reader URL and date scope; fetch the canonical reward endpoint with your own idempotency key.\n- `campaign.allocated` - New campaign allocated to a device\n- `payout.processed` - Revenue payout processed\n- `sensing.threshold_crossed` - A Sense Anything Route's threshold was crossed on one of your screens (custom sensed field)\n- `audience.spike` - A Route on a viewer-count signal crossed its threshold\n- `audience.venue_busy` - A Route on an occupancy signal crossed its threshold\n- `audience.purchase_intent` - A Route on a purchase-intent signal crossed its threshold\n- `audience.demographics_update` - A Route on a demographics signal crossed its threshold\n- `venue.traffic_summary` - A scheduled-digest Route's window elapsed (periodic traffic summary)\n- `programmatic.ad_started` - Programmatic ad started\n- `programmatic.ad_ended` - Programmatic ad ended\n- `programmatic.no_fill` - Programmatic no-fill response\n- `programmatic.error` - Programmatic error\n- `screen_group.created` - A Screen Group was created\n- `screen_group.updated` - A Screen Group's name/description was updated\n- `screen_group.deleted` - A Screen Group was deleted\n- `screen_group.member.added` - One or more screens were added to a Screen Group\n- `screen_group.member.removed` - One or more screens were removed from a Screen Group\n- `screen_group.content_policy.updated` - A Screen Group's content policy was changed\n- `screen_group.sensing_config.updated` - A Screen Group's sensing config was changed\n- `fleet_command.completed` - A group fleet command job completed\n- `fleet_command.failed` - A group fleet command job terminated with an error\n\n**Security:**\n- HTTPS required in production\n- HMAC-SHA256 signature verification\n- Secret shown only once in response - store it securely!\n\n**Signature Verification:**\n\nEvery webhook delivery carries a Stripe-style compound signature header:\n\n```\nX-Trillboards-Signature: t=<unix_seconds>,v0=<hex>,v1=<hex>\nX-Trillboards-Timestamp: <unix_seconds>\n```\n\nWhere:\n- `v1 = HMAC-SHA256(webhook_secret, \"${t}.${payload}\")` — canonical, replay-protected\n- `v0 = HMAC-SHA256(webhook_secret, payload)` — legacy body-only digest emitted\n  during the migration window for partners on pre-0.3.0 verifiers.\n\nRecommended: use `@trillboards/api-client@>=0.3.0`:\n\n```js\nimport { Webhooks } from '@trillboards/api-client';\nconst event = Webhooks.constructEvent(req.body, req.headers, process.env.WEBHOOK_SECRET);\n```\n\nFull contract + replay-window guidance: https://api.trillboards.com/docs/integrations/webhook-signature-verification.md\n",
        "operationId": "createWebhook",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "events": {
                    "description": "Events to subscribe to",
                    "example": [
                      "device.online",
                      "device.offline",
                      "impression.recorded"
                    ],
                    "items": {
                      "enum": [
                        "device.online",
                        "device.offline",
                        "device.requirements_updated",
                        "impression.recorded",
                        "creative.rewards_ready",
                        "campaign.allocated",
                        "payout.processed",
                        "sensing.threshold_crossed",
                        "audience.spike",
                        "audience.venue_busy",
                        "audience.purchase_intent",
                        "audience.demographics_update",
                        "venue.traffic_summary",
                        "programmatic.ad_started",
                        "programmatic.ad_ended",
                        "programmatic.no_fill",
                        "programmatic.error",
                        "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",
                        "fleet_command.completed",
                        "fleet_command.failed"
                      ],
                      "type": "string"
                    },
                    "minItems": 1,
                    "type": "array"
                  },
                  "url": {
                    "description": "Webhook endpoint URL (HTTPS required in production)",
                    "example": "https://example.com/webhooks/trillboards",
                    "format": "uri",
                    "type": "string"
                  }
                },
                "required": [
                  "url",
                  "events"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "events": [
                      "device.online",
                      "device.offline"
                    ],
                    "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
                    "status": "active",
                    "url": "https://example.com/webhooks/trillboards",
                    "webhook_id": "wh_m4k7x9_a1b2c3d4e5f6"
                  },
                  "message": "Webhook created. Save the secret - it won't be shown again!",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "events": {
                          "items": {
                            "type": "string"
                          },
                          "type": "array"
                        },
                        "secret": {
                          "description": "Webhook signing secret (shown only once!)",
                          "example": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
                          "type": "string"
                        },
                        "status": {
                          "enum": [
                            "active",
                            "inactive"
                          ],
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "webhook_id": {
                          "example": "wh_m4k7x9_a1b2c3d4e5f6",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Webhook created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Create webhook subscription",
        "tags": [
          "Webhooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/webhooks\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\":\"https://example.com/webhooks/trillboards\",\"events\":[\"device.online\",\"device.offline\",\"impression.recorded\"]}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/webhooks', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"url\": \"https://example.com/webhooks/trillboards\",\n    \"events\": [\n      \"device.online\",\n      \"device.offline\",\n      \"impression.recorded\"\n    ]\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/webhooks\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"url\": \"https://example.com/webhooks/trillboards\",\n        \"events\": [\n            \"device.online\",\n            \"device.offline\",\n            \"impression.recorded\",\n        ],\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/webhooks');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'url' => 'https://example.com/webhooks/trillboards',\n        'events' => [\n            'device.online',\n            'device.offline',\n            'impression.recorded',\n        ],\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/webhooks/{webhookId}": {
      "delete": {
        "description": "Remove a webhook subscription.",
        "operationId": "deleteWebhook",
        "parameters": [
          {
            "in": "path",
            "name": "webhookId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "message": "Webhook deleted successfully",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "message": {
                      "example": "Webhook deleted",
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Webhook deleted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Delete webhook",
        "tags": [
          "Webhooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X DELETE \"https://api.trillboards.com/v1/partner/webhooks/<webhookId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/webhooks/<webhookId>', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst body = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.delete(\n    \"https://api.trillboards.com/v1/partner/webhooks/<webhookId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\nbody = response.json()\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/webhooks/<webhookId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$body = json_decode($response, true);\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      },
      "patch": {
        "description": "Update webhook URL, events, or status.",
        "operationId": "updateWebhook",
        "parameters": [
          {
            "in": "path",
            "name": "webhookId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "events": {
                    "items": {
                      "enum": [
                        "device.online",
                        "device.offline",
                        "device.requirements_updated",
                        "impression.recorded",
                        "creative.rewards_ready",
                        "campaign.allocated",
                        "payout.processed",
                        "sensing.threshold_crossed",
                        "audience.spike",
                        "audience.venue_busy",
                        "audience.purchase_intent",
                        "audience.demographics_update",
                        "venue.traffic_summary",
                        "programmatic.ad_started",
                        "programmatic.ad_ended",
                        "programmatic.no_fill",
                        "programmatic.error",
                        "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",
                        "fleet_command.completed",
                        "fleet_command.failed"
                      ],
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "status": {
                    "enum": [
                      "active",
                      "inactive"
                    ],
                    "type": "string"
                  },
                  "url": {
                    "format": "uri",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "events": [
                      "device.online",
                      "device.offline",
                      "impression.recorded",
                      "creative.rewards_ready"
                    ],
                    "status": "active",
                    "url": "https://example.com/webhooks/trillboards",
                    "webhook_id": "wh_m4k7x9_a1b2c3d4e5f6"
                  },
                  "message": "Webhook updated successfully",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Webhook"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Webhook updated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Update webhook",
        "tags": [
          "Webhooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X PATCH \"https://api.trillboards.com/v1/partner/webhooks/<webhookId>\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"status\":\"active\"}'\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/webhooks/<webhookId>', {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"status\": \"active\"\n  }),\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.patch(\n    \"https://api.trillboards.com/v1/partner/webhooks/<webhookId>\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n    json={\n        \"status\": \"active\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/webhooks/<webhookId>');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'PATCH',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'status' => 'active',\n    ]),\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/webhooks/{webhookId}/deliveries": {
      "get": {
        "description": "Get recent webhook delivery attempts for debugging.",
        "operationId": "getWebhookDeliveries",
        "parameters": [
          {
            "in": "path",
            "name": "webhookId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50,
              "maximum": 100,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "deliveries": [
                      {
                        "attempted_at": "2026-01-01T02:30:00.000Z",
                        "delivered_at": "2026-01-01T02:30:00.142Z",
                        "event_type": "device.online",
                        "http_status": 200,
                        "id": "507f1f77bcf86cd799439011",
                        "response_time_ms": 142,
                        "status": "success"
                      },
                      {
                        "attempted_at": "2026-01-01T02:25:00.000Z",
                        "error_message": "Connection timeout",
                        "event_type": "impression.recorded",
                        "http_status": 500,
                        "id": "507f1f77bcf86cd799439012",
                        "response_time_ms": 5012,
                        "retry_count": 2,
                        "status": "failed"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "deliveries": {
                          "items": {
                            "$ref": "#/components/schemas/WebhookDelivery"
                          },
                          "type": "array"
                        }
                      },
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Delivery history"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Get delivery history",
        "tags": [
          "Webhooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS \"https://api.trillboards.com/v1/partner/webhooks/<webhookId>/deliveries\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/webhooks/<webhookId>/deliveries', {\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.get(\n    \"https://api.trillboards.com/v1/partner/webhooks/<webhookId>/deliveries\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/webhooks/<webhookId>/deliveries');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/partner/webhooks/{webhookId}/test": {
      "post": {
        "description": "Send a test event to verify your webhook endpoint is working.\nThe test event uses the `test` event type with a sample payload.\n",
        "operationId": "testWebhook",
        "parameters": [
          {
            "in": "path",
            "name": "webhookId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "error": null,
                    "response_time_ms": 142,
                    "status": 200
                  },
                  "message": "Test webhook sent successfully",
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "properties": {
                        "error": {
                          "nullable": true,
                          "type": "string"
                        },
                        "response_time_ms": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Test result"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "summary": "Test webhook",
        "tags": [
          "Webhooks"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -X POST \"https://api.trillboards.com/v1/partner/webhooks/<webhookId>/test\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_API_KEY\"\n"
          },
          {
            "label": "Node.js",
            "lang": "node",
            "source": "const res = await fetch('https://api.trillboards.com/v1/partner/webhooks/<webhookId>/test', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.TRILLBOARDS_API_KEY}`,\n  },\n});\nif (!res.ok) throw new Error(`Trillboards API ${res.status}: ${await res.text()}`);\nconst { data } = await res.json();\n"
          },
          {
            "label": "Python",
            "lang": "python",
            "source": "import os\nimport requests\n\nresponse = requests.post(\n    \"https://api.trillboards.com/v1/partner/webhooks/<webhookId>/test\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['TRILLBOARDS_API_KEY']}\",\n    },\n)\nresponse.raise_for_status()\ndata = response.json()[\"data\"]\n"
          },
          {
            "label": "PHP",
            "lang": "php",
            "source": "$ch = curl_init('https://api.trillboards.com/v1/partner/webhooks/<webhookId>/test');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('TRILLBOARDS_API_KEY'),\n    ],\n]);\n$response = curl_exec($ch);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) { throw new RuntimeException(\"Trillboards API $status: $response\"); }\n$data = json_decode($response, true)['data'];\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "partner"
      }
    },
    "/v1/playground/{operation_id}": {
      "post": {
        "description": "Validates the request body against the Zod schema registered for the given operationId WITHOUT executing the actual endpoint. Returns validation results and operation metadata. Useful for AI agents and developers to pre-validate requests before sending them.",
        "operationId": "playgroundDryRun",
        "parameters": [
          {
            "description": "The operationId of the endpoint to validate against.",
            "in": "path",
            "name": "operation_id",
            "required": true,
            "schema": {
              "description": "The operationId of the endpoint to validate against.",
              "example": "dataApiListScreens",
              "maxLength": 64,
              "minLength": 1,
              "pattern": "^[a-zA-Z0-9_]+$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaygroundResult"
                }
              }
            },
            "description": "Validation result with operation metadata."
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": []
          },
          {
            "ApiKeyAuth": []
          }
        ],
        "summary": "Dry-run validate a request body against an operation schema",
        "tags": [
          "API Meta"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "Before calling POST /v2/sdk/enrich, validate your payload: POST /v1/playground/sdkEnrich with the body you intend to send.",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v1/requests/{request_id}": {
      "get": {
        "description": "Returns metadata about a recent API request made with the caller's API key. Backed by an in-memory cache with ~7 minute lookback. Returns 404 if the request ID is unknown, expired from cache, or belongs to a different API key.",
        "operationId": "getRequestMetadata",
        "parameters": [
          {
            "description": "Server-generated request ID to look up.",
            "in": "path",
            "name": "request_id",
            "required": true,
            "schema": {
              "description": "Server-generated request ID to look up.",
              "example": "req_a1b2c3d4e5f6789012345678901234ab",
              "pattern": "^req_[a-f0-9]{32}$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RequestMetadataResponse"
                }
              }
            },
            "description": "Request metadata found and ownership verified."
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": []
          },
          {
            "ApiKeyAuth": []
          }
        ],
        "summary": "Look up metadata for a recent API request",
        "tags": [
          "API Meta"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-llm-example": "After receiving an error response, call GET /v1/requests/{request_id} with the request_id from the error envelope to retrieve timing and error classification.",
        "x-trillboards-source": "meta",
        "x-trillboards-surface": "partner-api-generated"
      }
    },
    "/v2/csi/venue/{screenId}/history": {
      "get": {
        "description": "Returns CSI occupancy aggregated into 10-minute buckets over a look-back window (default 24 hours, max 168 hours / 7 days). Each bucket carries the average and peak occupant counts, the average motion score, and the number of CSI windows aggregated. Buckets are ordered most-recent-first. Requires the `sdk:csi` API-key scope.",
        "operationId": "csiVenueHistory",
        "parameters": [
          {
            "description": "Mongo ObjectId of the screen / venue sensing node (24-char lowercase hex).",
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "description": "Mongo ObjectId of the screen / venue sensing node (24-char lowercase hex).",
              "example": "65a1b2c3d4e5f60718293a4b",
              "maxLength": 24,
              "minLength": 24,
              "pattern": "^[a-fA-F0-9]{24}$",
              "type": "string"
            }
          },
          {
            "description": "Look-back window in hours. Absent or non-positive values fall back to 24; values above 168 are capped to 168 (7 days).",
            "in": "query",
            "name": "hours",
            "required": false,
            "schema": {
              "default": 24,
              "description": "Look-back window in hours. Absent or non-positive values fall back to 24; values above 168 are capped to 168 (7 days).",
              "example": 24,
              "maximum": 168,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CsiApi.HistoryResponse"
                }
              }
            },
            "description": "Occupancy history for the requested screen and window."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "sdk:csi"
            ]
          },
          {
            "ApiKeyAuth": [
              "sdk:csi"
            ]
          }
        ],
        "summary": "Historical venue occupancy in 10-minute CSI buckets.",
        "tags": [
          "CTV SDK & CSI"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "sdk",
        "x-trillboards-surface": "sdk-api"
      }
    },
    "/v2/csi/venue/{screenId}/occupancy": {
      "get": {
        "description": "Returns the most recent CSI occupancy window (within the last 2 minutes) for a screen, derived from ESP32-S3 WiFi Channel State Information. The response is Redis-cached for 30s to support high-frequency polling. When an active window exists the body carries occupantCount, motionScore, signalQuality, hardwareType, the window bounds, and dataAge; otherwise it returns `status: \"no_data\"` with a human-readable message. Requires the `sdk:csi` API-key scope.",
        "operationId": "csiVenueOccupancy",
        "parameters": [
          {
            "description": "Mongo ObjectId of the screen / venue sensing node (24-char lowercase hex).",
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "description": "Mongo ObjectId of the screen / venue sensing node (24-char lowercase hex).",
              "example": "65a1b2c3d4e5f60718293a4b",
              "maxLength": 24,
              "minLength": 24,
              "pattern": "^[a-fA-F0-9]{24}$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CsiApi.OccupancyResponse"
                }
              }
            },
            "description": "Latest occupancy window, or a no_data envelope when nothing is available."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "sdk:csi"
            ]
          },
          {
            "ApiKeyAuth": [
              "sdk:csi"
            ]
          }
        ],
        "summary": "Real-time venue occupancy from WiFi CSI sensing.",
        "tags": [
          "CTV SDK & CSI"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "sdk",
        "x-trillboards-surface": "sdk-api"
      }
    },
    "/v2/device/ota/manifest": {
      "get": {
        "description": "Returns an Ed25519-signed manifest describing the set of agent APKs available for over-the-air delivery. The `agent` query parameter filters to a specific agent type; when omitted the caller receives the full bundle.",
        "operationId": "deviceOtaManifest",
        "parameters": [
          {
            "description": "Optional agent type filter (`agent-core`, `agent-core-lite`, etc.). When omitted the caller receives the default manifest bundle.",
            "in": "query",
            "name": "agent",
            "required": false,
            "schema": {
              "description": "Optional agent type filter (`agent-core`, `agent-core-lite`, etc.). When omitted the caller receives the default manifest bundle.",
              "example": "agent-core",
              "maxLength": 64,
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceApi.OtaManifestResponse"
                }
              }
            },
            "description": "Signed OTA manifest."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [],
        "summary": "Signed OTA manifest for managed-device update clients.",
        "tags": [
          "Device Provisioning"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "device",
        "x-trillboards-surface": "device-api"
      }
    },
    "/v2/earner/cameras/sensing-profile": {
      "get": {
        "description": "The BYO-CCTV connector polls this per feed to learn which fields its on-device VLM must emit, reaching parity with the cloud VLM's profile-driven schema. The screen is taken from the presented device token, never from the request, so a caller can only ever read its own screen. RTSP credentials and internal profile config never leave the server. A screen with no deployed profile — and a transient profile-read failure — both answer 200 with an empty profile, so sensing degrades to the connector's default schema instead of going silent.",
        "operationId": "deviceSensingProfile",
        "parameters": [
          {
            "description": "Device JWT minted at pair time; binds the caller to exactly one screen. REQUIRED, but `getDeviceToken` also accepts `X-Trillboard-Device-Token`, a `deviceToken` / `device_token` body field, or a `device_token` query param. Absent from all of them -> 401 `device_token_required`.",
            "in": "header",
            "name": "X-Device-Token",
            "required": false,
            "schema": {
              "description": "Device JWT minted at pair time; binds the caller to exactly one screen. REQUIRED, but `getDeviceToken` also accepts `X-Trillboard-Device-Token`, a `deviceToken` / `device_token` body field, or a `device_token` query param. Absent from all of them -> 401 `device_token_required`.",
              "type": "string"
            }
          },
          {
            "description": "Unix epoch milliseconds. REQUIRED, but `getRequestTimestampMs` also accepts `X-Request-Timestamp`, a `requestTimestamp` / `timestamp` / `sentAt` body field, a `timestamp` query param, or the standard `Date` header. Absent -> 400 `missing_device_timestamp`; outside the replay skew window -> 401 `stale_device_request`.",
            "in": "header",
            "name": "X-Device-Timestamp",
            "required": false,
            "schema": {
              "description": "Unix epoch milliseconds. REQUIRED, but `getRequestTimestampMs` also accepts `X-Request-Timestamp`, a `requestTimestamp` / `timestamp` / `sentAt` body field, a `timestamp` query param, or the standard `Date` header. Absent -> 400 `missing_device_timestamp`; outside the replay skew window -> 401 `stale_device_request`.",
              "example": "1755993600000",
              "type": "string"
            }
          },
          {
            "description": "Per-request UUID. REQUIRED, but `getRequestNonce` also accepts `X-Request-Nonce`, `X-Request-Id`, a `nonce` / `requestNonce` / `requestId` / `request_id` / `event_id` body field, or a `nonce` query param. Absent -> 400 `missing_device_nonce`; a repeat within the skew window -> 409 `replayed_device_request`.",
            "in": "header",
            "name": "X-Device-Nonce",
            "required": false,
            "schema": {
              "description": "Per-request UUID. REQUIRED, but `getRequestNonce` also accepts `X-Request-Nonce`, `X-Request-Id`, a `nonce` / `requestNonce` / `requestId` / `request_id` / `event_id` body field, or a `nonce` query param. Absent -> 400 `missing_device_nonce`; a repeat within the skew window -> 409 `replayed_device_request`.",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceApi.SensingProfileResponse"
                }
              }
            },
            "description": "The active profile's device-emittable fields, its capture cadence, and the absence-watched subset. Same shape whether or not a profile is deployed."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceApi.DeviceAuthRejection"
                }
              }
            },
            "description": "Replay guard incomplete — `missing_device_timestamp` or `missing_device_nonce` (replay guard is mandatory in production)."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceApi.SensingProfileAuthError"
                }
              }
            },
            "description": "`device_token_required`, `invalid_device_token`, `device_not_found`, or `stale_device_request` (timestamp outside the replay skew window). Also the handler's own `DEVICE_AUTH_REQUIRED` envelope when middleware admitted the request but left no bound screen on it."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceApi.DeviceAuthRejection"
                }
              }
            },
            "description": "`device_identity_mismatch` (the token did not resolve the device) or `device_token_subject_mismatch` (the token's subject is not the resolved device)."
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceApi.DeviceAuthRejection"
                }
              }
            },
            "description": "`replayed_device_request` — this nonce was already seen inside the skew window."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceApi.DeviceAuthRejection"
                }
              }
            },
            "description": "The auth middleware itself threw (an identity or persistence dependency refused) and `requireDeviceRequestAuth`'s catch served the same rejection envelope. Note its `error` carries the raw exception message, NOT a machine-readable reason code like the refusals above. The HANDLER cannot produce a 500: its own catch answers 200 with an empty profile so a profile-read failure never silences sensing."
          },
          "503": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceApi.DeviceAuthRejection"
                }
              }
            },
            "description": "`replay_guard_unavailable` — Redis could not answer, so the request is refused rather than admitted unguarded (there is no local nonce fallback)."
          }
        },
        "security": [
          {
            "DeviceToken__sensing": []
          }
        ],
        "summary": "Active sensing profile for the calling connector's bound screen.",
        "tags": [
          "Device Provisioning"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "device",
        "x-trillboards-surface": "device-api"
      }
    },
    "/v2/sdk/enrich": {
      "post": {
        "description": "Resolves the client IP into IAB income/age segments, education tier, connection type, and a stable HMAC household identifier. The `sdk:enrich` scope is required.",
        "operationId": "sdkEnrich",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SdkApi.EnrichBody"
              }
            }
          },
          "description": "Optional body — callers may supply a precomputed geohash6.",
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkApi.EnrichResponse"
                }
              }
            },
            "description": "Enriched demographic segments."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "sdk:enrich"
            ]
          },
          {
            "ApiKeyAuth": [
              "sdk:enrich"
            ]
          }
        ],
        "summary": "Census + demographic enrichment from the request IP.",
        "tags": [
          "CTV SDK & CSI"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "sdk",
        "x-trillboards-surface": "sdk-api"
      }
    },
    "/v2/sdk/impression": {
      "post": {
        "description": "Writes a sdk_impressions row with GIVT dedup, frequency counting, and reach cache updates. The `sdk:impression` scope is required.",
        "operationId": "sdkImpression",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SdkApi.ImpressionBody"
              }
            }
          },
          "description": "Full impression payload with viewability + fingerprint signals.",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkApi.ImpressionResponse"
                }
              }
            },
            "description": "Impression ingested."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "sdk:impression"
            ]
          },
          {
            "ApiKeyAuth": [
              "sdk:impression"
            ]
          }
        ],
        "summary": "Ingest an enriched CTV/DOOH impression.",
        "tags": [
          "CTV SDK & CSI"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "sdk",
        "x-trillboards-surface": "sdk-api"
      }
    },
    "/v2/sdk/qr/beacon": {
      "post": {
        "description": "Attaches device fingerprinting signals (canvas hash, WebGL renderer, network info, etc.) to an already-recorded scan. The `sdk:qr` scope is required.",
        "operationId": "sdkQrBeacon",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SdkApi.QrBeaconBody"
              }
            }
          },
          "description": "Beacon signal payload.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkApi.QrBeaconResponse"
                }
              }
            },
            "description": "Beacon signals merged."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "sdk:qr"
            ]
          },
          {
            "ApiKeyAuth": [
              "sdk:qr"
            ]
          }
        ],
        "summary": "Process beacon signals associated with a prior QR scan.",
        "tags": [
          "CTV SDK & CSI"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "sdk",
        "x-trillboards-surface": "sdk-api"
      }
    },
    "/v2/sdk/qr/scan": {
      "post": {
        "description": "Writes a qr_scan_events row tying a scan back to a specific screen, ad, and placement. The `sdk:qr` scope is required.",
        "operationId": "sdkQrScan",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SdkApi.QrScanBody"
              }
            }
          },
          "description": "QR scan attribution payload.",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkApi.QrScanResponse"
                }
              }
            },
            "description": "Scan recorded."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "BearerAuth__sdk": [
              "sdk:qr"
            ]
          },
          {
            "ApiKeyAuth": [
              "sdk:qr"
            ]
          }
        ],
        "summary": "Record a QR-code scan for physical-to-digital attribution.",
        "tags": [
          "CTV SDK & CSI"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "sdk",
        "x-trillboards-surface": "sdk-api"
      }
    },
    "/v2/sensing/paired-training-samples": {
      "post": {
        "description": "Used by the Android `CsiPairedDataCollector` to upload ~500 paired samples per call. Authentication is device-token (`X-Device-Token`). Idempotent on `sampleId` — duplicate samples in-batch and across batches are silently dropped.",
        "operationId": "sensingPairedTrainingUpload",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SensingApi.PairedTrainingBody"
              }
            }
          },
          "description": "Paired training sample batch.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SensingApi.PairedTrainingResponse"
                }
              }
            },
            "description": "Batch accepted; counts describe the per-category outcome."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": [
          {
            "DeviceToken__sensing": []
          }
        ],
        "summary": "Upload a batch of paired camera/CSI training samples.",
        "tags": [
          "Sensing"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "sensing",
        "x-trillboards-surface": "sensing-api"
      }
    }
  },
  "servers": [
    {
      "description": "Production API",
      "url": "https://api.trillboards.com"
    }
  ],
  "tags": [
    {
      "description": "Partner account management",
      "name": "Partners"
    },
    {
      "description": "Device registration and management",
      "name": "Devices"
    },
    {
      "description": "Create and manage screen groups (fleets) and their shared configuration",
      "name": "Screen Groups"
    },
    {
      "description": "Submit creative URLs for AI classification/moderation and read the structured result",
      "name": "Creative Moderation"
    },
    {
      "description": "Post log-level plays for ads you serve yourself; join them to classification + audience moments",
      "name": "Measurement Plays"
    },
    {
      "description": "Device-token doors the partner SDK's host hooks call (reportPlay, setTripState, reportInteraction); the screen is the device's own binding",
      "name": "SDK Host Hooks"
    },
    {
      "description": "Ad serving endpoints",
      "name": "Ads"
    },
    {
      "description": "Impression tracking",
      "name": "Impressions"
    },
    {
      "description": "Analytics and reporting",
      "name": "Analytics"
    },
    {
      "description": "Content management for partner screens",
      "name": "CMS"
    },
    {
      "description": "Webhook subscriptions for real-time event notifications",
      "name": "Webhooks"
    },
    {
      "description": "Dashboard overview and screen-level analytics",
      "name": "Dashboard"
    },
    {
      "description": "Earnings, transactions, and payout management",
      "name": "Earnings"
    },
    {
      "description": "Team member management for partner organizations",
      "name": "Team"
    },
    {
      "description": "Direct VAST integration endpoints for partners who want to fetch ads directly from Google Ad Manager.\n\nThis is the recommended approach for enterprise partners with existing video players.\nPartners cache the VAST config, build URLs client-side, and report impressions in batch.\n\n**Architecture:** Trillboards provides config + tracking only. Partners fetch VAST directly from Google.\n",
      "name": "VAST Integration"
    },
    {
      "description": "High-volume impression tracking with cryptographic proof generation.\n\nDesigned for partners with 10,000+ screens who need efficient batch reporting.\nEach impression receives an Ed25519 signature for third-party verification.\n",
      "name": "Batch Tracking"
    },
    {
      "description": "Fleet-wide device management for partners with large screen networks.\n\nPush commands, update settings, and view analytics across your entire fleet.\nSupports filtering by venue type, country, and specific screen IDs.\n",
      "name": "Fleet Management"
    },
    {
      "description": "Stripe Connect integration for partner payouts.\n\nCreate a Stripe Connect account, complete onboarding, and receive payouts\ndirectly to your bank account. Minimum payout is $5.00.\n",
      "name": "Stripe Connect"
    },
    {
      "description": "SDK partner dashboard API for earnings, screen health, and analytics.\n\nProvides pre-aggregated data optimized for embedding in partner dashboards.\n",
      "name": "Connect Dashboard"
    },
    {
      "description": "Real-world venue intelligence from CTV audience sensing.\n\nAccess foot traffic patterns, atmosphere data, nearby venues, and network-wide trends.\nData is aggregated from on-device face detection, audio analysis, and environmental sensors.\n",
      "name": "Venue Intelligence"
    },
    {
      "description": "Advanced audience analytics including live data, heatmaps, lookalike screens, and predictions.\n\nLookalike and prediction endpoints require usage-based billing (`requireBilling('data_api')`).\n",
      "name": "Audience Intelligence",
      "x-beta": true
    },
    {
      "description": "Custom audience segment targeting for programmatic campaigns.\n\nCreate segments based on demographics, venue type, geography, and audience behavior.\nMatch segments to screens and generate targeted VAST tags.\n\nRequires usage-based billing (`requireBilling('data_api')`).\n",
      "name": "Audience Segments",
      "x-beta": true
    },
    {
      "description": "Intent-based pricing catalog for audience purchase intent signals.\n\nBrowse intent categories and get real-time intent pricing for screens\nbased on live audience signals (speech, emotion, behavior).\n",
      "name": "Intent Catalog",
      "x-beta": true
    },
    {
      "description": "Testing tools for partner API integration.\n\nCreate sandbox screens, generate test events, and validate webhook integrations\nwithout affecting production data. Sandbox data expires after 24 hours.\n",
      "name": "Sandbox",
      "x-beta": true
    },
    {
      "description": "API usage tracking, rate limit status, and usage dashboards",
      "name": "Usage"
    },
    {
      "description": "Server-Sent Events (SSE) for real-time event streaming.\n\nSubscribe to live events from your screens including audience updates,\ndevice status changes, and impression notifications.\nMaximum 5 concurrent SSE connections per partner.\n",
      "name": "Events"
    },
    {
      "description": "White-label network configuration for content preferences, waterfall settings, and branding",
      "name": "Network Config"
    },
    {
      "description": "Usage-based billing and credit management.\n\nTrillboards uses a pay-per-use model with generous free tiers. No subscriptions required.\nWhen free tier limits are exceeded, endpoints return 402 Payment Required until billing\nis set up via Stripe. Credits can be purchased at volume discounts.\n",
      "name": "Billing"
    },
    {
      "description": "Self-service registration for AI agents and automated systems.\n\nAgents can register without human intervention, receive an API key and sandbox screens,\nand begin making API calls immediately within free tier limits.\n",
      "name": "Agent Registration"
    },
    {
      "description": "On-glass CTV measurement: CSI occupancy, enrichment, impression and QR-scan reporting. Requires an `sdk:*` scope on the partner key.",
      "name": "CTV SDK & CSI"
    },
    {
      "description": "Edge sensing ingestion. Authenticated with a per-device token, not the partner key.",
      "name": "Sensing"
    },
    {
      "description": "OTA model manifests and CCTV connector pairing. Authenticated with a per-device token.",
      "name": "Device Provisioning"
    },
    {
      "description": "Federated model updates from the fleet — gradient, performance and taste uploads, plus the public model-read endpoints.",
      "name": "Federated Learning"
    },
    {
      "description": "The self-describing surface: the machine-readable error catalog, the changelog, the request-metadata lookup and the schema playground. Unauthenticated except where noted.",
      "name": "API Meta"
    },
    {
      "description": "Funded native creative optimization with measured play feedback.",
      "name": "Creative Feedback"
    }
  ],
  "x-trillboards-bundle": {
    "slug": "partner",
    "sources": [
      {
        "file": "partner-api.yaml",
        "key": "partner"
      },
      {
        "file": "generated/sdk-api.json",
        "key": "sdk"
      },
      {
        "file": "generated/sensing-api.json",
        "key": "sensing"
      },
      {
        "file": "generated/device-api.json",
        "key": "device"
      },
      {
        "file": "generated/federated-api.json",
        "key": "federated"
      },
      {
        "file": "generated/partner-api-generated.json",
        "key": "meta"
      }
    ]
  }
}
