{
  "components": {
    "parameters": {
      "campaignId": {
        "description": "Campaign identifier",
        "in": "path",
        "name": "campaignId",
        "required": true,
        "schema": {
          "pattern": "^[a-zA-Z0-9_-]{1,128}$",
          "type": "string"
        }
      },
      "experimentId": {
        "description": "Incrementality experiment UUID",
        "in": "path",
        "name": "experimentId",
        "required": true,
        "schema": {
          "format": "uuid",
          "type": "string"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Invalid request parameters"
      },
      "Forbidden": {
        "content": {
          "application/json": {
            "example": {
              "message": "Not authorized to access this campaign",
              "success": false
            },
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Not authorized to access this resource"
      },
      "NotFound": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Resource not found"
      },
      "NotFound__proof": {
        "content": {
          "application/json": {
            "example": {
              "message": "Campaign not found",
              "success": false
            },
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Resource not found"
      },
      "ProofCursorRetired": {
        "content": {
          "application/json": {
            "example": {
              "code": "proof_cursor_retired",
              "message": "proof-of-play cursor was issued by the retired PostgreSQL path; restart the export from page 1",
              "success": false
            },
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/LegacyCodedError"
                },
                {
                  "properties": {
                    "code": {
                      "enum": [
                        "proof_cursor_retired"
                      ],
                      "type": "string"
                    },
                    "success": {
                      "enum": [
                        false
                      ],
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              ]
            }
          }
        },
        "description": "The `cursor` was issued by the retired PostgreSQL proof path (it begins\n`pg~`) and cannot be continued: proofs are read from ClickHouse only.\nNot retryable with the same cursor — restart from page one (omit\n`cursor`).\n"
      },
      "ProofStoreUnavailable": {
        "content": {
          "application/json": {
            "example": {
              "code": "ch_unavailable",
              "error": "ch_unavailable",
              "label": "event_tracking.proofOfPlay.campaignProofs"
            },
            "schema": {
              "properties": {
                "code": {
                  "enum": [
                    "ch_unavailable"
                  ],
                  "type": "string"
                },
                "error": {
                  "enum": [
                    "ch_unavailable"
                  ],
                  "type": "string"
                },
                "label": {
                  "description": "Internal name of the read that failed. Quote it to support.",
                  "type": "string"
                }
              },
              "required": [
                "error",
                "code"
              ],
              "type": "object"
            }
          }
        },
        "description": "The proof store (ClickHouse) could not be read. Retryable with backoff.\nProofs are never answered as an empty page, a zero aggregate or a\ndenied screen when the store could not be read.\n"
      },
      "ProofWindowOutOfRetention": {
        "content": {
          "application/json": {
            "example": {
              "code": "out_of_retention",
              "error": "out_of_retention",
              "label": "event_tracking.proofOfPlay.campaignProofs",
              "message": "event_tracking.proofOfPlay.campaignProofs: the requested window ends before retained history begins (2026-06-26T18:40:00.000Z)",
              "retained_from": "2026-06-26T18:40:00.000Z"
            },
            "schema": {
              "properties": {
                "code": {
                  "enum": [
                    "out_of_retention"
                  ],
                  "type": "string"
                },
                "error": {
                  "enum": [
                    "out_of_retention"
                  ],
                  "type": "string"
                },
                "label": {
                  "description": "Internal name of the read that refused. Quote it to support.",
                  "type": "string"
                },
                "message": {
                  "type": "string"
                },
                "retained_from": {
                  "description": "The earliest instant the proof store still holds.",
                  "format": "date-time",
                  "type": "string"
                }
              },
              "required": [
                "error",
                "code",
                "retained_from"
              ],
              "type": "object"
            }
          }
        },
        "description": "The requested window (`endDate`) ends before the proof store's retained\nhistory begins (`retained_from`; the store keeps the last 90 days). Not\nretryable with the same window: ask for a window that ends on or after\n`retained_from`. A window that only STARTS before it is served from\n`retained_from` on. Proofs are never answered as an empty page for a\nwindow the store no longer holds.\n"
      },
      "ProviderImpressionAccepted": {
        "description": "Accepted. Empty body. Also returned for a non-attributable line and for an\ninternal error — failures are never surfaced to the ad server.\n"
      },
      "ProviderImpressionInvalidRef": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "example": "Invalid ref",
                  "type": "string"
                }
              },
              "type": "object"
            }
          }
        },
        "description": "The `t` reference is missing or its HMAC did not verify."
      },
      "ProviderImpressionNotFound": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "Not found",
                    "Unknown ref"
                  ],
                  "type": "string"
                }
              },
              "type": "object"
            }
          }
        },
        "description": "Either the intake is disabled (`{\"error\":\"Not found\"}`) or the reference\nverified but resolved to no line (`{\"error\":\"Unknown ref\"}`).\n"
      },
      "ServerError": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Internal server error"
      },
      "Unauthorized": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "description": "Missing or invalid authentication token"
      },
      "Unauthorized__proof": {
        "content": {
          "application/json": {
            "example": {
              "message": "Authentication required",
              "success": false
            },
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Missing or invalid authentication token"
      }
    },
    "schemas": {
      "AdjustPostbackRequest": {
        "additionalProperties": true,
        "description": "Adjust callback payload (provider-native snake_case). Additional Adjust\nfields are accepted and ignored.\n",
        "properties": {
          "activity_kind": {
            "description": "Adjust activity kind; used as the event-name fallback.",
            "type": "string"
          },
          "adid": {
            "description": "Adjust device id; part of the dedup key.",
            "type": "string"
          },
          "campaign_id": {
            "type": "string"
          },
          "campaign_name": {
            "description": "Fallback for campaign_id.",
            "type": "string"
          },
          "created_at": {
            "description": "Event unix timestamp; part of the dedup key.",
            "type": "string"
          },
          "event_name": {
            "type": "string"
          },
          "event_token": {
            "description": "Event type token; part of the dedup key.",
            "type": "string"
          },
          "gps_adid": {
            "description": "Raw GAID (hashed at the edge).",
            "type": "string"
          },
          "idfa": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "AdvertiserCategoryCount": {
        "properties": {
          "category": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "AppsflyerPostbackRequest": {
        "additionalProperties": true,
        "description": "AppsFlyer postback payload (provider-native snake_case). Additional\nAppsFlyer fields are accepted and ignored.\n",
        "properties": {
          "advertising_id": {
            "description": "Raw GAID (hashed at the edge to maid_hash).",
            "type": "string"
          },
          "af_c_id": {
            "description": "AppsFlyer campaign id (fallback for campaign_id).",
            "type": "string"
          },
          "appsflyer_id": {
            "description": "Per-install id; dedup key with event_time when event_id is absent.",
            "type": "string"
          },
          "campaign_id": {
            "type": "string"
          },
          "event_id": {
            "description": "Per-event id (newer templates); primary dedup key.",
            "type": "string"
          },
          "event_name": {
            "description": "AppsFlyer event name; defaults to `install` when absent.",
            "type": "string"
          },
          "event_time": {
            "description": "Event timestamp (\"YYYY-MM-DD HH:MM:SS\" UTC, or unix seconds/millis).",
            "type": "string"
          },
          "idfa": {
            "description": "Raw iOS IDFA (hashed at the edge).",
            "type": "string"
          }
        },
        "type": "object"
      },
      "AttentionMetrics": {
        "description": "Attention-verified impression metrics from FEIN edge AI.\nEvery field is MEASURED by on-device computer vision, not estimated.\n",
        "properties": {
          "attention": {
            "properties": {
              "attention_qualified_pct": {
                "description": "Percentage of impressions with attention_score >= 0.5",
                "type": "number"
              },
              "avg_attention_score": {
                "description": "Mean fraction of faces looking at screen across all impressions",
                "maximum": 1,
                "minimum": 0,
                "type": "number"
              },
              "avg_dwell_seconds": {
                "description": "Average time faces were in view of the screen",
                "type": "number"
              },
              "avg_face_count": {
                "description": "Average number of faces detected per impression",
                "type": "number"
              },
              "median_attention_score": {
                "type": "number"
              },
              "p90_attention_score": {
                "description": "90th percentile — the attention floor for top 10% of impressions",
                "type": "number"
              }
            },
            "type": "object"
          },
          "breakdowns": {
            "properties": {
              "daypart_breakdown": {
                "additionalProperties": {
                  "type": "object"
                },
                "type": "object"
              },
              "venue_type_breakdown": {
                "additionalProperties": {
                  "type": "object"
                },
                "type": "object"
              }
            },
            "type": "object"
          },
          "campaign_id": {
            "type": "string"
          },
          "economics": {
            "description": "Attention-adjusted pricing metrics",
            "properties": {
              "attention_cpm": {
                "description": "aCPM = cost / (SUM(attention_score * face_count) / 1000).\nThe true cost of an attention-verified impression.\n",
                "type": "number"
              },
              "cost_per_attentive_reach": {
                "type": "number"
              },
              "cost_per_attentive_visit": {
                "type": "number"
              },
              "standard_cpm": {
                "description": "Traditional CPM (cost per 1,000 impressions)",
                "type": "number"
              }
            },
            "type": "object"
          },
          "emotion": {
            "properties": {
              "avg_emotion_engagement": {
                "type": "number"
              },
              "emotion_breakdown": {
                "additionalProperties": {
                  "type": "number"
                },
                "type": "object"
              },
              "positive_emotion_pct": {
                "description": "Percentage of impressions with happy/surprised/interested emotions",
                "type": "number"
              }
            },
            "type": "object"
          },
          "total_impressions": {
            "type": "integer"
          },
          "unique_devices": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "AttentionTimeseriesPoint": {
        "description": "One day of campaign attention, grouped by `metric_date`.",
        "properties": {
          "attentionCpm": {
            "description": "CPM over attention-qualified impressions only. `null` when unpriced.",
            "nullable": true,
            "type": "number"
          },
          "attentionQualifiedPct": {
            "description": "Share of impressions clearing the attention bar.",
            "nullable": true,
            "type": "number"
          },
          "avgAttentionScore": {
            "description": "0.0-1.0.",
            "nullable": true,
            "type": "number"
          },
          "avgDwellSeconds": {
            "description": "Mean dwell during playback.",
            "nullable": true,
            "type": "number"
          },
          "avgEmotionEngagement": {
            "description": "0.0-1.0.",
            "nullable": true,
            "type": "number"
          },
          "avgFaceCount": {
            "description": "Mean faces in frame.",
            "nullable": true,
            "type": "number"
          },
          "date": {
            "description": "Measurement day. `metric_date` is a PG `DATE`; with no `pg` type parser installed it serialises as a full ISO-8601 timestamp at UTC midnight, NOT `YYYY-MM-DD`.",
            "type": "string"
          },
          "standardCpm": {
            "description": "CPM over all impressions. `null` when unpriced.",
            "nullable": true,
            "type": "number"
          },
          "totalImpressions": {
            "description": "`SUM(total_impressions)` for the day.",
            "type": "integer"
          },
          "uniqueDevices": {
            "description": "`SUM(unique_devices)` for the day.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "AttributionSummary": {
        "description": "Comprehensive campaign attribution summary with reach, footfall,\ncost, and quality metrics. This is the single object that proves\nDOOH campaign ROI.\n",
        "properties": {
          "advertiser_id": {
            "type": "string"
          },
          "breakdowns": {
            "properties": {
              "daypart_breakdown": {
                "additionalProperties": {
                  "properties": {
                    "lift_pct": {
                      "type": "number"
                    },
                    "visitors": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                },
                "type": "object"
              },
              "venue_breakdown": {
                "additionalProperties": {
                  "properties": {
                    "lift_pct": {
                      "type": "number"
                    },
                    "visitors": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                },
                "type": "object"
              }
            },
            "type": "object"
          },
          "campaign_id": {
            "type": "string"
          },
          "cost": {
            "properties": {
              "cost_per_unique_reach": {
                "description": "Cost per unique device reached",
                "type": "number"
              },
              "total_media_cost": {
                "description": "Total campaign media spend",
                "type": "number"
              }
            },
            "type": "object"
          },
          "date_range": {
            "properties": {
              "end": {
                "format": "date",
                "type": "string"
              },
              "start": {
                "format": "date",
                "type": "string"
              }
            },
            "type": "object"
          },
          "footfall": {
            "description": "Footfall attribution — proving ads drove store visits",
            "properties": {
              "control_visit_rate": {
                "description": "control_visitors / control_group_size",
                "type": "number"
              },
              "control_visitors": {
                "description": "Unexposed control group devices that visited a POI",
                "type": "integer"
              },
              "cost_per_incremental_visit": {
                "description": "total_media_cost / incremental_visits (CPIV)",
                "type": "number"
              },
              "exposed_visit_rate": {
                "description": "exposed_visitors / unique_devices_exposed",
                "type": "number"
              },
              "exposed_visitors": {
                "description": "Exposed devices that visited a POI within attribution window",
                "type": "integer"
              },
              "incremental_lift_pct": {
                "description": "Percentage increase in visit rate attributable to the campaign.\nFormula: (exposed_visit_rate - control_visit_rate) / control_visit_rate * 100\n",
                "type": "number"
              },
              "incremental_visits": {
                "description": "Visits that would NOT have happened without the campaign",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "quality": {
            "description": "Data quality and statistical confidence indicators",
            "properties": {
              "attribution_window_days": {
                "description": "Attribution lookback window in days",
                "type": "integer"
              },
              "avg_match_confidence": {
                "description": "Average geospatial match confidence score",
                "maximum": 1,
                "minimum": 0,
                "type": "number"
              },
              "avg_time_to_visit_h": {
                "description": "Average hours from exposure to store visit",
                "type": "number"
              },
              "control_group_size": {
                "description": "Size of DMA-matched unexposed control group",
                "type": "integer"
              },
              "statistical_significance": {
                "description": "P(lift > 0) from Bayesian model. > 0.95 = significant.",
                "maximum": 1,
                "minimum": 0,
                "type": "number"
              }
            },
            "type": "object"
          },
          "reach": {
            "properties": {
              "avg_frequency": {
                "description": "Average exposures per unique device",
                "type": "number"
              },
              "total_exposures": {
                "description": "Total exposure events (one device can have multiple)",
                "type": "integer"
              },
              "unique_devices_exposed": {
                "description": "Count of unique MAID hashes exposed to the campaign",
                "type": "integer"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "AttributionTimeseriesPoint": {
        "properties": {
          "avg_match_confidence": {
            "type": "number"
          },
          "control_visitors": {
            "type": "integer"
          },
          "cost_per_incremental_visit": {
            "type": "number"
          },
          "exposed_visitors": {
            "type": "integer"
          },
          "incremental_lift_pct": {
            "type": "number"
          },
          "incremental_visits": {
            "type": "integer"
          },
          "statistical_significance": {
            "type": "number"
          },
          "summary_date": {
            "format": "date",
            "type": "string"
          },
          "total_exposures": {
            "type": "integer"
          },
          "unique_devices_exposed": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "AudienceData": {
        "properties": {
          "attentionScore": {
            "description": "Average attention score (0-1)",
            "type": "number"
          },
          "faceCount": {
            "description": "Number of faces detected",
            "type": "integer"
          },
          "incomeLevel": {
            "enum": [
              "low",
              "medium",
              "high",
              "premium"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "AudienceOverindex": {
        "description": "Aggregated demographic over-index across a campaign's screens (Census\nACS × FEIN edge-AI fusion). Over-index > 1.0 means the segment appears\nmore often than the neighborhood baseline.\n",
        "properties": {
          "ageOverindex": {
            "additionalProperties": {
              "type": "number"
            },
            "description": "Over-index keyed by age bucket (e.g. `25-34`).",
            "type": "object"
          },
          "campaignId": {
            "type": "string"
          },
          "daysAnalyzed": {
            "type": "integer"
          },
          "genderOverindex": {
            "additionalProperties": {
              "type": "number"
            },
            "description": "Over-index keyed by gender bucket (male/female).",
            "type": "object"
          },
          "screenCount": {
            "type": "integer"
          },
          "significantSegments": {
            "items": {
              "$ref": "#/components/schemas/OverindexSegment"
            },
            "type": "array"
          },
          "topOverindexSegment": {
            "nullable": true,
            "type": "string"
          },
          "topOverindexValue": {
            "type": "number"
          }
        },
        "type": "object"
      },
      "AudienceProfile": {
        "description": "Per-screen DSP-consensus audience profile inherited from the bid stream.",
        "properties": {
          "avgBidCpm": {
            "type": "number"
          },
          "bidPartnerCount": {
            "description": "Distinct demand partners observed bidding on the screen.",
            "type": "integer"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "daypart": {
            "type": "string"
          },
          "demandScore": {
            "description": "Composite demand score (CPM, partner diversity, deal %, fill).",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "medianBidCpm": {
            "type": "number"
          },
          "profileDate": {
            "format": "date",
            "type": "string"
          },
          "profileId": {
            "format": "uuid",
            "type": "string"
          },
          "screenMongoId": {
            "type": "string"
          },
          "segmentConsensus": {
            "items": {
              "$ref": "#/components/schemas/SegmentConsensus"
            },
            "type": "array"
          },
          "topAdvertiserCategories": {
            "items": {
              "$ref": "#/components/schemas/AdvertiserCategoryCount"
            },
            "type": "array"
          },
          "totalBids": {
            "type": "integer"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "AudienceProofsResponse": {
        "properties": {
          "data": {
            "properties": {
              "aggregateSignature": {
                "type": "string"
              },
              "campaignId": {
                "type": "string"
              },
              "generatedAt": {
                "format": "date-time",
                "type": "string"
              },
              "privacyMode": {
                "enum": [
                  "aggregated"
                ],
                "type": "string"
              },
              "queryPeriod": {
                "type": "object"
              },
              "screenBreakdown": {
                "items": {
                  "properties": {
                    "impressions": {
                      "type": "integer"
                    },
                    "period": {
                      "properties": {
                        "first": {
                          "format": "date-time",
                          "type": "string"
                        },
                        "last": {
                          "format": "date-time",
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "screenId": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                },
                "type": "array"
              },
              "signatureVersion": {
                "type": "string"
              },
              "totalImpressions": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "success": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "AutoOptimizeConfig": {
        "properties": {
          "autoOptimize": {
            "type": "boolean"
          },
          "campaignId": {
            "type": "string"
          },
          "explorationWeight": {
            "maximum": 0.5,
            "minimum": 0.05,
            "type": "number"
          },
          "minSampleSize": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "AutoOptimizeRequest": {
        "properties": {
          "enabled": {
            "description": "Enable (true) or disable (false) auto-optimization.",
            "type": "boolean"
          },
          "explorationWeight": {
            "description": "Impression-share floor reserved per creative (must be 0.05–0.50).",
            "maximum": 0.5,
            "minimum": 0.05,
            "type": "number"
          },
          "minSampleSize": {
            "description": "Minimum impressions before a creative's data is trusted (default 100).",
            "type": "integer"
          }
        },
        "required": [
          "enabled"
        ],
        "type": "object"
      },
      "BroadcasterMetric": {
        "description": "Per-broadcaster reach + lift for cross-screen attribution.",
        "properties": {
          "broadcaster_id": {
            "description": "ATSC 3.0 serviceId or HbbTV CCID (or DVB triple composite).",
            "type": "string"
          },
          "channel_ccid": {
            "description": "DVB canonical content ID.",
            "type": "string"
          },
          "lift_pct": {
            "description": "Cross-screen incremental lift attributable to TV exposure.",
            "format": "float",
            "type": "number"
          },
          "tv_exposed_households": {
            "description": "Count of households exposed via broadcast.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "CampaignProofsResponse": {
        "properties": {
          "data": {
            "properties": {
              "campaignId": {
                "type": "string"
              },
              "generatedAt": {
                "format": "date-time",
                "type": "string"
              },
              "pagination": {
                "$ref": "#/components/schemas/Pagination"
              },
              "proofs": {
                "items": {
                  "$ref": "#/components/schemas/Proof"
                },
                "type": "array"
              },
              "proofsReturned": {
                "type": "integer"
              },
              "publicKeyUrl": {
                "type": "string"
              },
              "queryPeriod": {
                "properties": {
                  "endDate": {
                    "type": "string"
                  },
                  "startDate": {
                    "type": "string"
                  }
                },
                "type": "object"
              },
              "signatureVersion": {
                "type": "string"
              },
              "totalImpressions": {
                "description": "Total count (only on first page)",
                "nullable": true,
                "type": "integer"
              }
            },
            "type": "object"
          },
          "success": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "CausalAggregateLift": {
        "description": "Campaign-wide causal lift with 95% bootstrap CI (camelCase fields).",
        "properties": {
          "ciHigh": {
            "description": "Upper bound of the 95% bootstrap confidence interval.",
            "type": "number"
          },
          "ciLow": {
            "description": "Lower bound of the 95% bootstrap confidence interval.",
            "type": "number"
          },
          "controlN": {
            "description": "Control sample size behind the estimate.",
            "type": "integer"
          },
          "exposedN": {
            "description": "Exposed sample size behind the estimate.",
            "type": "integer"
          },
          "lift": {
            "description": "Point estimate of the average treatment effect (visit-rate lift).",
            "type": "number"
          }
        },
        "type": "object"
      },
      "CausalLiftReport": {
        "description": "Per-segment heterogeneous treatment effect (causal lift) report from the\ndaily X-learner model. Segment keys are `dimension:value` (e.g.\n`age:25_34`); the campaign-wide aggregate is surfaced separately.\n",
        "properties": {
          "aggregateLift": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CausalAggregateLift"
              }
            ],
            "description": "Campaign-wide (all-segments) lift, or null when unavailable.",
            "nullable": true,
            "type": "object"
          },
          "campaignId": {
            "type": "string"
          },
          "measuredDate": {
            "description": "Measurement date of the surfaced estimates, or null if none.",
            "format": "date",
            "nullable": true,
            "type": "string"
          },
          "methodologyVersion": {
            "description": "X-learner methodology version the estimates were produced with.",
            "example": 2,
            "type": "integer"
          },
          "perSegmentLifts": {
            "items": {
              "$ref": "#/components/schemas/CausalSegmentLift"
            },
            "type": "array"
          },
          "topResponsiveSegment": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CausalTopSegment"
              }
            ],
            "description": "Segment with the highest lift.",
            "nullable": true,
            "type": "object"
          },
          "topUnresponsiveSegment": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CausalTopSegment"
              }
            ],
            "description": "Segment with the lowest lift.",
            "nullable": true,
            "type": "object"
          }
        },
        "type": "object"
      },
      "CausalSegmentLift": {
        "description": "Per-segment causal lift (snake_case fields, as emitted).",
        "properties": {
          "ci_high": {
            "type": "number"
          },
          "ci_low": {
            "type": "number"
          },
          "control_n": {
            "type": "integer"
          },
          "exposed_n": {
            "type": "integer"
          },
          "lift": {
            "type": "number"
          },
          "segment": {
            "description": "Segment key, `dimension:value` (e.g. `age:25_34`).",
            "type": "string"
          }
        },
        "type": "object"
      },
      "CausalTopSegment": {
        "description": "The most- or least-responsive segment.",
        "properties": {
          "ci_high": {
            "type": "number"
          },
          "ci_low": {
            "type": "number"
          },
          "lift": {
            "type": "number"
          },
          "segment": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "ChannelCredit": {
        "properties": {
          "attributed_conversions": {
            "type": "integer"
          },
          "attributed_revenue_cents": {
            "type": "integer"
          },
          "credit_pct": {
            "description": "Percentage of total attribution credit",
            "type": "number"
          }
        },
        "type": "object"
      },
      "ChannelROAS": {
        "description": "Credit-weighted conversion VALUE by channel, from `attribution_chains`.\n\n**This endpoint does not compute ROAS.** It has no media-cost input and returns no ratio — the name is historical. Divide `channelValue` by your own spend to get a return figure.",
        "properties": {
          "avgTimeToConversionHours": {
            "description": "Mean first-touch-to-conversion gap in hours.",
            "nullable": true,
            "type": "number"
          },
          "channelValue": {
            "description": "Per-channel `ROUND(SUM(<channel>_credit * COALESCE(conversion_value_cents, 0)))` — CENTS, integer. A channel with no credit column populated reports 0, which is indistinguishable from a channel that earned no credit.",
            "properties": {
              "audio": {
                "type": "integer"
              },
              "ctv": {
                "type": "integer"
              },
              "display": {
                "type": "integer"
              },
              "dooh": {
                "type": "integer"
              },
              "mobile": {
                "type": "integer"
              },
              "olv": {
                "type": "integer"
              },
              "store": {
                "type": "integer"
              },
              "web": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "conversions": {
            "description": "Chains with a non-null `conversion_at`.",
            "type": "integer"
          },
          "totalChains": {
            "description": "`COUNT(*)` of attribution chains for the campaign.",
            "type": "integer"
          },
          "totalConversionValue": {
            "description": "`SUM(conversion_value_cents)` — CENTS, integer.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "CohortExport": {
        "properties": {
          "exportId": {
            "type": "string"
          },
          "maidCount": {
            "description": "Number of unique MAIDs in the export",
            "type": "integer"
          },
          "s3Url": {
            "description": "Pre-signed S3 URL for the exported MAID list (expires in 24h)",
            "format": "uri",
            "type": "string"
          }
        },
        "type": "object"
      },
      "CohortRefreshRequest": {
        "properties": {
          "destination": {
            "description": "Target DSP to re-sync the exposed cohort to (routed via Intuizi).",
            "enum": [
              "ttd",
              "dv360",
              "meta",
              "cadent",
              "mediaocean"
            ],
            "type": "string"
          }
        },
        "required": [
          "destination"
        ],
        "type": "object"
      },
      "CohortRefreshResult": {
        "description": "Result of a forced cohort re-sync routed through Intuizi.",
        "properties": {
          "destination": {
            "enum": [
              "ttd",
              "dv360",
              "meta",
              "cadent",
              "mediaocean"
            ],
            "type": "string"
          },
          "durationMs": {
            "type": "integer"
          },
          "maidCount": {
            "description": "Number of identifiers handed to Intuizi.",
            "type": "integer"
          },
          "routedVia": {
            "description": "Always `intuizi` — DSP activation is fanned out on Intuizi's graph.",
            "example": "intuizi",
            "type": "string"
          },
          "s3Url": {
            "description": "Pre-signed S3 URL of the handed-off cohort file.",
            "format": "uri",
            "type": "string"
          },
          "segmentId": {
            "format": "uuid",
            "type": "string"
          },
          "status": {
            "description": "Activation status.",
            "example": "activated",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ConversionSource": {
        "description": "A measurement connection, REDACTED. Reads are filtered through an ALLOWLIST, so a column not named there never reaches a client — the OAuth tokens and the previous webhook secret never appear at all, and webhook_secret appears only as its last 4 characters.",
        "properties": {
          "active": {
            "type": "boolean"
          },
          "domain": {
            "description": "NOT always a domain. For ga4 it is the property resource name, for google_ads the customer id, for meta `act_<id>`.",
            "nullable": true,
            "type": "string"
          },
          "last_event_at": {
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "last_sync_detail": {
            "description": "The adapter's own reason string, written for the advertiser to read.",
            "nullable": true,
            "type": "string"
          },
          "last_sync_outcome": {
            "nullable": true,
            "type": "string"
          },
          "mmp_provider": {
            "description": "Required when source_type is `mmp`, null otherwise.",
            "enum": [
              "appsflyer",
              "adjust",
              "kochava",
              "branch",
              "singular"
            ],
            "nullable": true,
            "type": "string"
          },
          "oauth_status": {
            "description": "The only OAuth fact a client sees. NULL on an OAuth provider means the connect flow was never finished.",
            "nullable": true,
            "type": "string"
          },
          "source_id": {
            "example": "cs_9f2a41c0",
            "type": "string"
          },
          "source_type": {
            "enum": [
              "shopify",
              "pos_webhook",
              "pixel",
              "server_postback",
              "mmp",
              "search_console",
              "store_visits",
              "qr",
              "ga4",
              "meta",
              "google_ads"
            ],
            "type": "string"
          },
          "verification_state": {
            "description": "Moves only in-band — a source proves itself by receiving.",
            "type": "string"
          },
          "webhook_secret_last4": {
            "nullable": true,
            "type": "string"
          },
          "webhook_secret_previous_expires_at": {
            "description": "When a rolled-away secret stops being accepted. The only roll field a client may read — the previous secret itself never leaves the service.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          }
        },
        "type": "object"
      },
      "CreateExperimentRequest": {
        "properties": {
          "campaignId": {
            "pattern": "^[a-zA-Z0-9_-]{1,128}$",
            "type": "string"
          },
          "controlDmas": {
            "description": "DMA codes for control (ads withheld)",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "experimentType": {
            "enum": [
              "geo_holdout",
              "ghost_ad",
              "psa_control",
              "matched_market"
            ],
            "type": "string"
          },
          "holdoutPct": {
            "default": 0.1,
            "description": "Fraction of audience to withhold ads from",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "targetAlpha": {
            "default": 0.05,
            "description": "Significance level (probability of false positive)",
            "type": "number"
          },
          "targetMde": {
            "default": 0.1,
            "description": "Minimum detectable effect (e.g. 0.10 = 10% lift)",
            "type": "number"
          },
          "targetPower": {
            "default": 0.8,
            "description": "Statistical power (probability of detecting true effect)",
            "type": "number"
          },
          "treatmentDmas": {
            "description": "DMA codes for treatment (ads shown)",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "campaignId",
          "experimentType"
        ],
        "type": "object"
      },
      "CreatedConversionSource": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ConversionSource"
          },
          {
            "properties": {
              "deactivated_sources": {
                "description": "Empty on the common path. Non-empty means an existing source of the same scope stopped receiving because of this call.",
                "items": {
                  "$ref": "#/components/schemas/DeactivatedConversionSource"
                },
                "type": "array"
              },
              "webhook_secret": {
                "description": "Full, and shown exactly once — this response and the roll are the only two places it ever appears. Null for source types that do not sign their ingest.",
                "nullable": true,
                "type": "string"
              }
            },
            "type": "object"
          }
        ],
        "description": "The create response only. Everything ConversionSource carries, plus the one-time full webhook_secret and what this create displaced."
      },
      "CreativeAttentionComparison": {
        "description": "One creative's attention rollup, from `attention_campaign_metrics` grouped by `creative_id`.",
        "properties": {
          "attentionCpm": {
            "description": "`AVG(attention_cpm)` — CPM over attention-qualified impressions only. `null` when unpriced.",
            "nullable": true,
            "type": "number"
          },
          "attentionQualifiedPct": {
            "description": "Share of impressions clearing the attention bar.",
            "nullable": true,
            "type": "number"
          },
          "avgAttentionScore": {
            "description": "`AVG(avg_attention_score)`, 0.0-1.0. Rows are ordered by this, descending.",
            "nullable": true,
            "type": "number"
          },
          "avgDwellSeconds": {
            "description": "`AVG(avg_dwell_seconds)`.",
            "nullable": true,
            "type": "number"
          },
          "avgEmotionEngagement": {
            "description": "`AVG(avg_emotion_engagement)`, 0.0-1.0.",
            "nullable": true,
            "type": "number"
          },
          "avgFaceCount": {
            "description": "`AVG(avg_face_count)` — mean faces in frame during playback.",
            "nullable": true,
            "type": "number"
          },
          "creativeId": {
            "description": "Creative identifier.",
            "type": "string"
          },
          "positiveEmotionPct": {
            "description": "`AVG(positive_emotion_pct)`.",
            "nullable": true,
            "type": "number"
          },
          "totalImpressions": {
            "description": "`SUM(total_impressions)`.",
            "type": "integer"
          },
          "uniqueDevices": {
            "description": "`SUM(unique_devices)`.",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "CreativeAttributionPerformance": {
        "description": "One creative/variant rollup over `creative_attribution_performance`, ordered by `totalVisits` descending.",
        "properties": {
          "attention": {
            "properties": {
              "avgDwell": {
                "description": "`AVG(avg_dwell_seconds)`.",
                "nullable": true,
                "type": "number"
              },
              "avgEmotion": {
                "description": "`AVG(avg_emotion_engagement)`.",
                "nullable": true,
                "type": "number"
              },
              "avgScore": {
                "description": "`AVG(avg_attention_score)`.",
                "nullable": true,
                "type": "number"
              },
              "dominantEmotion": {
                "description": "`MODE() WITHIN GROUP` over the per-day dominant emotion.",
                "nullable": true,
                "type": "string"
              }
            },
            "type": "object"
          },
          "avgCostPerVisit": {
            "description": "`AVG(cost_per_visit)`. `null` when never priced.",
            "nullable": true,
            "type": "number"
          },
          "avgLiftPct": {
            "description": "`AVG(incremental_lift_pct)`. `null` when lift was never measured.",
            "nullable": true,
            "type": "number"
          },
          "avgVisitRate": {
            "description": "`AVG(visit_rate)`, 0.0-1.0.",
            "nullable": true,
            "type": "number"
          },
          "bestContext": {
            "description": "Modal best-performing context across the measured days.",
            "properties": {
              "daypart": {
                "nullable": true,
                "type": "string"
              },
              "venueType": {
                "nullable": true,
                "type": "string"
              },
              "weather": {
                "nullable": true,
                "type": "string"
              }
            },
            "type": "object"
          },
          "creativeId": {
            "description": "Creative identifier.",
            "type": "string"
          },
          "dateRange": {
            "properties": {
              "daysMeasured": {
                "description": "Days with a row — not the span between `first` and `last`.",
                "type": "integer"
              },
              "first": {
                "description": "`MIN(metric_date)`; a PG `DATE`, so it serialises as an ISO-8601 timestamp at UTC midnight.",
                "type": "string"
              },
              "last": {
                "description": "`MAX(metric_date)`, same serialisation.",
                "type": "string"
              }
            },
            "type": "object"
          },
          "totalVisits": {
            "description": "`SUM(store_visits)`.",
            "type": "integer"
          },
          "variant": {
            "description": "Creative variant. The rollup writes the literal `default` when a creative has no variants.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "CreativeAttributionTimeseriesPoint": {
        "description": "One day of a creative's attribution performance, ordered by `metric_date` ascending.",
        "properties": {
          "avgAttention": {
            "description": "`avg_attention_score`.",
            "nullable": true,
            "type": "number"
          },
          "avgDwell": {
            "description": "`avg_dwell_seconds`.",
            "nullable": true,
            "type": "number"
          },
          "avgEmotion": {
            "description": "`avg_emotion_engagement`.",
            "nullable": true,
            "type": "number"
          },
          "bestDaypart": {
            "nullable": true,
            "type": "string"
          },
          "bestVenue": {
            "description": "`best_venue_type` for the day.",
            "nullable": true,
            "type": "string"
          },
          "bestWeather": {
            "nullable": true,
            "type": "string"
          },
          "costPerVisit": {
            "description": "`cost_per_visit`. `null` carries the same never-measured meaning.",
            "nullable": true,
            "type": "number"
          },
          "date": {
            "description": "`metric_date`; a PG `DATE`, so it serialises as an ISO-8601 timestamp at UTC midnight.",
            "type": "string"
          },
          "liftPct": {
            "description": "`incremental_lift_pct`. **`null` means never measured** and is deliberately not coerced to 0 — a zero here would read as \"measured, no lift\".",
            "nullable": true,
            "type": "number"
          },
          "storeVisits": {
            "description": "Attributed store visits that day.",
            "type": "integer"
          },
          "visitRate": {
            "description": "Visits per exposed device, 0.0-1.0.",
            "nullable": true,
            "type": "number"
          }
        },
        "type": "object"
      },
      "CreativeWeight": {
        "properties": {
          "attentionScore": {
            "type": "number"
          },
          "confidence": {
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "type": "string"
          },
          "creativeId": {
            "type": "string"
          },
          "totalImpressions": {
            "type": "integer"
          },
          "totalVisits": {
            "type": "integer"
          },
          "visitRate": {
            "type": "number"
          },
          "weight": {
            "description": "Normalized impression share (weights sum to 1.0).",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          }
        },
        "type": "object"
      },
      "CrossChannelJourney": {
        "description": "Channel-transition graph for one campaign, from `crossChannelAttributionService.getCrossChannelJourney`. This is an edge list plus a per-channel rollup — not a Sankey node/link document.",
        "properties": {
          "channels": {
            "description": "Per-channel totals across the campaign.",
            "items": {
              "properties": {
                "channel": {
                  "description": "Channel name.",
                  "type": "string"
                },
                "touchpoints": {
                  "description": "`COUNT(*)` of touchpoints on this channel.",
                  "type": "integer"
                },
                "uniqueDevices": {
                  "description": "`COUNT(DISTINCT maid_hash)` on this channel.",
                  "type": "integer"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "flows": {
            "description": "Consecutive channel transitions, from a `LAG()` window over `touchpoint_events`. Capped at 50, ordered by `count` descending.",
            "items": {
              "properties": {
                "count": {
                  "description": "Transitions observed.",
                  "type": "integer"
                },
                "source": {
                  "description": "Channel of the preceding touchpoint.",
                  "type": "string"
                },
                "target": {
                  "description": "Channel of the following touchpoint.",
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "CrossScreenMetrics": {
        "description": "Cross-screen TV→DOOH attribution metrics. Returned only when the\nrequest includes `attributionModes=cross_screen_tv` (or `cross_screen_all`).\nSources:\n  - tv_exposed_households: PG `tv_exposed_household_daily` rollup\n  - tv_household_visit_rate: ratio of TV-exposed households that also\n    appear in `device_visitation` within attribution window\n  - tv_household_lift_pct: causal lift from causalLiftService v=2\n    tv_daypart segments\n  - broadcaster_breakdown: per-broadcaster reach + lift\n  - tv_daypart_breakdown: per-daypart_bucket reach + lift\n",
        "properties": {
          "broadcaster_breakdown": {
            "items": {
              "$ref": "#/components/schemas/BroadcasterMetric"
            },
            "type": "array"
          },
          "cross_screen_journey_pct": {
            "description": "Percentage of TV-exposed households that subsequently appeared at\na Trillboards-instrumented venue within the attribution window.\n",
            "format": "float",
            "type": "number"
          },
          "tv_daypart_breakdown": {
            "properties": {
              "evening": {
                "$ref": "#/components/schemas/DaypartLift"
              },
              "midday": {
                "$ref": "#/components/schemas/DaypartLift"
              },
              "morning": {
                "$ref": "#/components/schemas/DaypartLift"
              },
              "overnight": {
                "$ref": "#/components/schemas/DaypartLift"
              }
            },
            "type": "object"
          },
          "tv_exposed_households": {
            "description": "Total households exposed via broadcast in the attribution window.",
            "type": "integer"
          },
          "tv_household_lift_pct": {
            "description": "Incremental visit-rate lift attributable to TV exposure.",
            "format": "float",
            "type": "number"
          },
          "tv_household_visit_rate": {
            "description": "Ratio of TV-exposed households that visited a Trillboards venue.",
            "format": "float",
            "type": "number"
          }
        },
        "type": "object"
      },
      "DaypartBidModifier": {
        "properties": {
          "bidModifierPct": {
            "description": "Bid modifier percentage, capped at [-80, +200].",
            "maximum": 200,
            "minimum": -80,
            "type": "number"
          },
          "confidence": {
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "type": "string"
          },
          "conversionRate": {
            "type": "number"
          },
          "daypart": {
            "type": "string"
          },
          "sampleSize": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "DaypartLift": {
        "description": "Per-daypart X-learner lift estimate for the cross_screen_tv mode.",
        "properties": {
          "ci_high": {
            "format": "float",
            "type": "number"
          },
          "ci_low": {
            "description": "Lower bound of the 95% bootstrap CI from causalLiftService.",
            "format": "float",
            "type": "number"
          },
          "lift_pct": {
            "format": "float",
            "type": "number"
          },
          "tv_exposed_households": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "DeactivatedConversionSource": {
        "description": "A source this create turned off. `was_receiving` is the fact that matters: true means live postbacks stop being accepted now.",
        "properties": {
          "last_event_at": {
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "mmp_provider": {
            "enum": [
              "appsflyer",
              "adjust",
              "kochava",
              "branch",
              "singular"
            ],
            "nullable": true,
            "type": "string"
          },
          "source_id": {
            "example": "cs_9f2a41c0",
            "type": "string"
          },
          "was_receiving": {
            "description": "The displaced source was in verification_state 'receiving' at the moment it was deactivated.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "DisplayContext": {
        "properties": {
          "areaRatio": {
            "description": "Ratio of screen area used (0-1)",
            "type": "number"
          },
          "mode": {
            "description": "Display mode",
            "enum": [
              "fullscreen",
              "l-bar",
              "split"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "DowntimeExperiment": {
        "properties": {
          "campaignId": {
            "type": "string"
          },
          "controlMaidCount": {
            "type": "integer"
          },
          "controlVisitRate": {
            "type": "number"
          },
          "downtimeEnd": {
            "format": "date-time",
            "type": "string"
          },
          "downtimeMinutes": {
            "type": "integer"
          },
          "downtimeStart": {
            "format": "date-time",
            "type": "string"
          },
          "experimentId": {
            "format": "uuid",
            "type": "string"
          },
          "isSignificant": {
            "type": "boolean"
          },
          "liftPct": {
            "type": "number"
          },
          "pValue": {
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "screenMongoId": {
            "type": "string"
          },
          "treatmentMaidCount": {
            "type": "integer"
          },
          "treatmentVisitRate": {
            "type": "number"
          }
        },
        "type": "object"
      },
      "DowntimeExperiments": {
        "description": "Quasi-experimental lift measured from screen downtime windows.",
        "properties": {
          "avgLiftPct": {
            "type": "number"
          },
          "experiments": {
            "items": {
              "$ref": "#/components/schemas/DowntimeExperiment"
            },
            "type": "array"
          },
          "significantExperiments": {
            "type": "integer"
          },
          "totalExperiments": {
            "type": "integer"
          }
        },
        "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"
      },
      "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"
      },
      "ErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/LegacyError"
          },
          {
            "properties": {
              "success": {
                "enum": [
                  false
                ],
                "type": "boolean"
              }
            },
            "type": "object"
          }
        ],
        "description": "Legacy free-text error shape. COLLAPSED onto the shared `LegacyError`\ndefinition, narrowed to `success: false` — which is what this surface\nhas always documented and still returns.\n",
        "x-superseded-by": "TrillboardsError"
      },
      "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"
      },
      "Experiment": {
        "description": "An `incrementality_experiments` row, returned verbatim by `INSERT … RETURNING *`.",
        "properties": {
          "advertiser_id": {
            "description": "Taken from the caller's token, never from the request body.",
            "type": "string"
          },
          "caliper_width": {
            "description": "Matching caliper. Default 0.25.",
            "type": "number"
          },
          "campaign_id": {
            "description": "Campaign under measurement.",
            "type": "string"
          },
          "control_dmas": {
            "description": "Nielsen DMA codes in the control arm. Defaults to `[]`.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "created_at": {
            "format": "date-time",
            "type": "string"
          },
          "ended_at": {
            "description": "`null` until the experiment ends.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "experiment_id": {
            "description": "Server-assigned (`gen_random_uuid()`).",
            "format": "uuid",
            "type": "string"
          },
          "experiment_type": {
            "enum": [
              "geo_holdout",
              "ghost_ad",
              "psa_control",
              "matched_market"
            ],
            "type": "string"
          },
          "holdout_pct": {
            "description": "Share of the treatment population withheld. Default 0.10.",
            "type": "number"
          },
          "matching_covariates": {
            "additionalProperties": true,
            "description": "Covariates for matched-market designs. `{}` on create.",
            "type": "object"
          },
          "required_sample_size": {
            "description": "Always `null` from this route — the create handler never sets it.",
            "nullable": true,
            "type": "integer"
          },
          "started_at": {
            "description": "`null` until the experiment is started.",
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "status": {
            "description": "Always `draft` on create — the create route never sends a status, so the column default applies.",
            "enum": [
              "draft",
              "active",
              "measuring",
              "completed",
              "cancelled"
            ],
            "type": "string"
          },
          "target_alpha": {
            "description": "Default 0.05.",
            "type": "number"
          },
          "target_mde": {
            "description": "Minimum detectable effect. Default 0.10.",
            "type": "number"
          },
          "target_power": {
            "description": "Default 0.80.",
            "type": "number"
          },
          "treatment_dmas": {
            "description": "Nielsen DMA codes in the treatment arm. Defaults to `[]`.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "updated_at": {
            "description": "Equal to `created_at` on create.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ExperimentWithResults": {
        "description": "An experiment plus its measurement history.",
        "properties": {
          "experiment": {
            "$ref": "#/components/schemas/Experiment"
          },
          "latestResult": {
            "additionalProperties": true,
            "description": "Newest `incrementality_results` row, returned as the raw row (flat, snake_case) — NOT the nested shape used by the campaign incrementality endpoint. `null` when nothing has been measured.",
            "nullable": true,
            "type": "object"
          },
          "resultHistory": {
            "description": "Up to 30 raw `incrementality_results` rows, newest `measurement_date` first.",
            "items": {
              "additionalProperties": true,
              "type": "object"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "ExportProofsResponse": {
        "properties": {
          "data": {
            "properties": {
              "campaignId": {
                "type": "string"
              },
              "checksum": {
                "description": "SHA256 checksum of the export data",
                "type": "string"
              },
              "data": {
                "items": {
                  "$ref": "#/components/schemas/Proof"
                },
                "type": "array"
              },
              "expiresAt": {
                "format": "date-time",
                "type": "string"
              },
              "exportedAt": {
                "format": "date-time",
                "type": "string"
              },
              "format": {
                "type": "string"
              },
              "proofCount": {
                "type": "integer"
              },
              "publicKeyUrl": {
                "type": "string"
              },
              "signatureVersion": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "success": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "HeatmapPoint": {
        "properties": {
          "conversion_count": {
            "description": "Number of POI visits from exposed devices in this area",
            "type": "integer"
          },
          "density": {
            "description": "Relative density (0-1) normalized across all clusters",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "exposure_count": {
            "description": "Number of device exposures in this geographic cluster",
            "type": "integer"
          },
          "geohash": {
            "description": "Geohash-6 cell identifier",
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          }
        },
        "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"
      },
      "InboundDelivery": {
        "description": "One thing a provider PUSHED, and what we answered. request_body is redacted of device identifiers at write time.",
        "properties": {
          "delivery_id": {
            "format": "uuid",
            "type": "string"
          },
          "http_status": {
            "type": "integer"
          },
          "is_test": {
            "type": "boolean"
          },
          "outcome": {
            "enum": [
              "accepted",
              "deduplicated",
              "rejected",
              "error"
            ],
            "type": "string"
          },
          "outcome_detail": {
            "description": "Our 4xx message verbatim. Never a 5xx body — that one is generic by design.",
            "nullable": true,
            "type": "string"
          },
          "received_at": {
            "format": "date-time",
            "type": "string"
          },
          "request_body": {
            "additionalProperties": true,
            "nullable": true,
            "type": "object"
          },
          "route": {
            "description": "The route PATTERN, never the resolved URL.",
            "type": "string"
          },
          "touchpoint_id": {
            "format": "uuid",
            "nullable": true,
            "type": "string"
          }
        },
        "type": "object"
      },
      "IncrementalityResult": {
        "description": "One incrementality experiment on a campaign, with its newest measurement.",
        "properties": {
          "controlDmas": {
            "description": "Nielsen DMA codes in the control arm.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "experimentId": {
            "format": "uuid",
            "type": "string"
          },
          "latestResult": {
            "description": "Newest `incrementality_results` row for the experiment, or `null` before the first measurement lands.",
            "nullable": true,
            "properties": {
              "control": {
                "properties": {
                  "devices": {
                    "type": "integer"
                  },
                  "rate": {
                    "description": "Visit rate, 0.0-1.0.",
                    "nullable": true,
                    "type": "number"
                  },
                  "visitors": {
                    "type": "integer"
                  }
                },
                "type": "object"
              },
              "credibleInterval": {
                "properties": {
                  "lower": {
                    "nullable": true,
                    "type": "number"
                  },
                  "upper": {
                    "nullable": true,
                    "type": "number"
                  }
                },
                "type": "object"
              },
              "date": {
                "description": "`measurement_date`; a PG `DATE`, so it serialises as an ISO-8601 timestamp at UTC midnight.",
                "type": "string"
              },
              "expectedUplift": {
                "description": "Posterior mean uplift.",
                "nullable": true,
                "type": "number"
              },
              "incrementalVisits": {
                "description": "Visits attributed to exposure above the control baseline.",
                "nullable": true,
                "type": "number"
              },
              "lift": {
                "description": "`relative_lift_pct` — percentage points, not a ratio.",
                "nullable": true,
                "type": "number"
              },
              "pValue": {
                "description": "Frequentist p-value for the lift.",
                "nullable": true,
                "type": "number"
              },
              "posteriorProbPositive": {
                "description": "Bayesian posterior probability that lift > 0.",
                "nullable": true,
                "type": "number"
              },
              "treatment": {
                "properties": {
                  "devices": {
                    "type": "integer"
                  },
                  "rate": {
                    "description": "Visit rate, 0.0-1.0.",
                    "nullable": true,
                    "type": "number"
                  },
                  "visitors": {
                    "type": "integer"
                  }
                },
                "type": "object"
              }
            },
            "type": "object"
          },
          "status": {
            "description": "Experiment lifecycle state.",
            "type": "string"
          },
          "treatmentDmas": {
            "description": "Nielsen DMA codes in the treatment arm.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "type": {
            "description": "`experiment_type`.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "JourneyFunnel": {
        "description": "Exposure-to-visit funnel for one campaign. Emitted by `attributionReportingService.getCampaignJourney`.",
        "properties": {
          "avgTimeToVisitHours": {
            "description": "Mean exposure-to-visit gap. `null` when no outcome row has fired yet.",
            "nullable": true,
            "type": "number"
          },
          "funnel": {
            "properties": {
              "impressions": {
                "description": "`COUNT(*)` over `exposure_device_matches`. Falls back to a `play_logs` count when that is 0.",
                "type": "integer"
              },
              "uniqueExposed": {
                "description": "`COUNT(DISTINCT maid_hash)` over `exposure_device_matches`.",
                "type": "integer"
              },
              "visited14d": {
                "description": "Same, `<= 336`. A superset of `visited7d`, not a disjoint bucket.",
                "type": "integer"
              },
              "visited7d": {
                "description": "Distinct devices in `attribution_outcomes` with `time_to_visit_h <= 168`.",
                "type": "integer"
              }
            },
            "type": "object"
          },
          "medianTimeToVisitHours": {
            "description": "`PERCENTILE_CONT(0.5)` of the same gap. `null` on an empty panel.",
            "nullable": true,
            "type": "number"
          }
        },
        "type": "object"
      },
      "KochavaPostbackRequest": {
        "additionalProperties": true,
        "description": "Kochava postback payload (provider-native snake_case). Additional\nKochava fields are accepted and ignored.\n",
        "properties": {
          "adid": {
            "type": "string"
          },
          "campaign_id": {
            "type": "string"
          },
          "campaign_name": {
            "type": "string"
          },
          "device_id": {
            "description": "Raw device id (hashed at the edge).",
            "type": "string"
          },
          "event_id": {
            "description": "Stable per-event id; dedup key.",
            "type": "string"
          },
          "event_name": {
            "type": "string"
          },
          "event_time": {
            "type": "string"
          },
          "timestamp": {
            "type": "string"
          }
        },
        "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"
      },
      "LoyaltyTierStats": {
        "properties": {
          "avgFrequency": {
            "type": "number"
          },
          "avgLoyaltyScore": {
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "deviceCount": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Methodology": {
        "description": "Human-readable explanation of how attribution was computed\nfor a specific campaign. Full methodology transparency.\n",
        "properties": {
          "attention_enrichment": {
            "properties": {
              "description": {
                "type": "string"
              },
              "method": {
                "type": "string"
              },
              "signals": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "type": "object"
          },
          "campaign_id": {
            "type": "string"
          },
          "cross_channel": {
            "properties": {
              "channels": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "description": {
                "type": "string"
              },
              "half_life_hours": {
                "type": "number"
              },
              "method": {
                "type": "string"
              },
              "model": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "exposure_matching": {
            "properties": {
              "confidence_model": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "max_distance_m": {
                "type": "number"
              },
              "method": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "incrementality": {
            "properties": {
              "control_group_matching": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "experiment_type": {
                "type": "string"
              },
              "method": {
                "type": "string"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "MmpPostbackRequest": {
        "additionalProperties": true,
        "description": "Per-source MMP postback payload — the recognized field union across all\nfive supported providers (AppsFlyer, Adjust, Kochava, Branch,\nSingular). Any extra provider field is accepted and ignored.\n\nProviders disagree on the name of every field that matters, so each\ngroup below is read in the order listed and the FIRST present value\nwins. The device-identifier order is load-bearing: it decides which id\na touchpoint keys on, and it is the same list the inbound delivery log\nredacts, so a field readable here is never a raw device id in storage.\n",
        "properties": {
          "activity_kind": {
            "description": "Adjust's name for the event.",
            "type": "string"
          },
          "adid": {
            "description": "Adjust device id. Persistent and device-scoped, so it is hashed, never stored.",
            "type": "string"
          },
          "advertising_id": {
            "description": "GAID (Adjust, Kochava).",
            "type": "string"
          },
          "af_c_id": {
            "description": "AppsFlyer campaign id.",
            "type": "string"
          },
          "af_revenue": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ]
          },
          "aifa": {
            "description": "GAID (Singular).",
            "type": "string"
          },
          "appsflyer_id": {
            "description": "AppsFlyer per-install id. Persistent and device-scoped, so it is hashed, never stored.",
            "type": "string"
          },
          "campaign_id": {
            "type": "string"
          },
          "campaign_name": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "currency": {
            "description": "ISO-4217.",
            "type": "string"
          },
          "device_id": {
            "type": "string"
          },
          "event_id": {
            "description": "Provider-native stable event id. First choice for the dedup key, which\nis what makes a provider retry exactly-once.\n",
            "type": "string"
          },
          "event_name": {
            "description": "Event name. Falls back to `activity_kind`, then to `install`.",
            "type": "string"
          },
          "event_revenue": {
            "description": "AppsFlyer revenue, in the currency's MAIN UNIT (e.g. \"12.50\"), never cents.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ]
          },
          "event_revenue_currency": {
            "description": "ISO-4217. Anything other than USD (or absent) stores no revenue.",
            "type": "string"
          },
          "event_time": {
            "type": "string"
          },
          "google_advertising_id": {
            "description": "GAID (Branch).",
            "type": "string"
          },
          "gps_adid": {
            "description": "GAID (Adjust's Google Play name).",
            "type": "string"
          },
          "idfa": {
            "description": "Apple IDFA. Sent under this name by every provider.",
            "type": "string"
          },
          "revenue": {
            "description": "Adjust/Kochava revenue, main unit.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ]
          },
          "revenue_currency": {
            "description": "ISO-4217.",
            "type": "string"
          },
          "revenue_float": {
            "description": "Adjust revenue, main unit.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ]
          },
          "sum": {
            "description": "Singular revenue, main unit.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ]
          },
          "timestamp": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "MobileEventRequest": {
        "properties": {
          "advertiserId": {
            "type": "string"
          },
          "campaignId": {
            "pattern": "^[a-zA-Z0-9_-]{1,128}$",
            "type": "string"
          },
          "clickUrl": {
            "format": "uri",
            "type": "string"
          },
          "creativeId": {
            "type": "string"
          },
          "dspPartner": {
            "description": "DSP name (e.g. \"thetradedesk\", \"dv360\")",
            "type": "string"
          },
          "eventType": {
            "default": "impression",
            "enum": [
              "impression",
              "click",
              "complete"
            ],
            "type": "string"
          },
          "maidHash": {
            "description": "SHA-256 hash of the mobile advertising ID",
            "type": "string"
          },
          "placementId": {
            "type": "string"
          }
        },
        "required": [
          "maidHash",
          "campaignId"
        ],
        "type": "object"
      },
      "MultiTouchAttribution": {
        "description": "Cross-channel credit allocation showing how much each channel\ncontributed to conversions.\n",
        "properties": {
          "campaign_id": {
            "type": "string"
          },
          "channel_credits": {
            "properties": {
              "dooh": {
                "$ref": "#/components/schemas/ChannelCredit"
              },
              "mobile_ad": {
                "$ref": "#/components/schemas/ChannelCredit"
              },
              "store_visit": {
                "$ref": "#/components/schemas/ChannelCredit"
              },
              "web_visit": {
                "$ref": "#/components/schemas/ChannelCredit"
              }
            },
            "type": "object"
          },
          "model": {
            "description": "Attribution model used for credit allocation",
            "enum": [
              "time_decay",
              "position_based",
              "attention_weighted",
              "shapley"
            ],
            "type": "string"
          },
          "total_attributed_revenue_cents": {
            "type": "integer"
          },
          "total_conversions": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "NotConfiguredError": {
        "description": "Deliberately NOT collapsed onto `LegacyError`: this shape's `error` field\ncarries a machine token (`not_configured`), not the free-text message\n`LegacyError.error` holds. Folding the two together would misdescribe\nboth. `TrillboardsError` is where that token belongs — as `error.code`.\n",
        "properties": {
          "error": {
            "description": "Machine token identifying the unconfigured subsystem.",
            "example": "not_configured",
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "type": "object",
        "x-superseded-by": "TrillboardsError"
      },
      "NotMeasuredOutcome": {
        "description": "Why a response carries no measured value: nothing measures this\nquantity (its inputs or store were retired). This is neither a failed\nread (which answers 503) nor a read that found nothing.\n",
        "properties": {
          "kind": {
            "enum": [
              "not_measured"
            ],
            "type": "string"
          },
          "reason": {
            "description": "Machine-readable reason. Switch on `kind`, not on this text.",
            "example": "inputs_frozen_in_postgres_no_clickhouse_join",
            "type": "string"
          },
          "what": {
            "description": "The quantity that is not measured.",
            "example": "weather_incrementality",
            "type": "string"
          }
        },
        "required": [
          "kind",
          "what",
          "reason"
        ],
        "type": "object"
      },
      "OptimizationReport": {
        "description": "Attribution-driven creative weights, venue/daypart bid modifiers, and auto-optimization config.",
        "properties": {
          "autoOptimize": {
            "description": "Whether nightly auto-optimization is enabled for the campaign.",
            "type": "boolean"
          },
          "campaignId": {
            "type": "string"
          },
          "computedAt": {
            "format": "date-time",
            "nullable": true,
            "type": "string"
          },
          "creativeWeights": {
            "items": {
              "$ref": "#/components/schemas/CreativeWeight"
            },
            "type": "array"
          },
          "daypartModifiers": {
            "items": {
              "$ref": "#/components/schemas/DaypartBidModifier"
            },
            "type": "array"
          },
          "explanation": {
            "description": "Human-readable rationale for the current weights and modifiers.",
            "type": "string"
          },
          "explorationWeight": {
            "description": "Minimum impression-share floor reserved per creative.",
            "maximum": 0.5,
            "minimum": 0.05,
            "type": "number"
          },
          "minSampleSize": {
            "description": "Minimum impressions before a creative's data is trusted.",
            "type": "integer"
          },
          "venueModifiers": {
            "items": {
              "$ref": "#/components/schemas/VenueBidModifier"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "OverindexSegment": {
        "description": "A segment whose over-index clears the significance threshold (≥ 1.5).",
        "properties": {
          "overindex": {
            "type": "number"
          },
          "segment": {
            "type": "string"
          },
          "type": {
            "enum": [
              "age",
              "gender"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "Pagination": {
        "properties": {
          "hasMore": {
            "description": "Whether more results exist",
            "type": "boolean"
          },
          "limit": {
            "description": "Requested limit",
            "type": "integer"
          },
          "nextCursor": {
            "description": "Cursor for next page (null if no more results)",
            "nullable": true,
            "type": "string"
          }
        },
        "type": "object"
      },
      "PostbackAck": {
        "properties": {
          "status": {
            "example": "ok",
            "type": "string"
          }
        },
        "type": "object"
      },
      "PostbackDedupAck": {
        "properties": {
          "deduped": {
            "description": "True when the postback matched a prior event and was collapsed (no new touchpoint).",
            "type": "boolean"
          },
          "status": {
            "example": "ok",
            "type": "string"
          }
        },
        "type": "object"
      },
      "PowerAnalysis": {
        "properties": {
          "alpha": {
            "type": "number"
          },
          "baselineRate": {
            "type": "number"
          },
          "mde": {
            "type": "number"
          },
          "power": {
            "type": "number"
          },
          "required_sample_per_group": {
            "description": "Minimum sample size needed per group (treatment + control)",
            "type": "integer"
          },
          "total_sample_needed": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "Proof": {
        "properties": {
          "adId": {
            "description": "Campaign/Advertisement ID",
            "type": "string"
          },
          "audience": {
            "$ref": "#/components/schemas/AudienceData"
          },
          "deviceId": {
            "description": "Device identifier",
            "type": "string"
          },
          "displayContext": {
            "$ref": "#/components/schemas/DisplayContext"
          },
          "eventType": {
            "description": "VAST event type",
            "enum": [
              "impression",
              "start",
              "firstQuartile",
              "midpoint",
              "thirdQuartile",
              "complete"
            ],
            "type": "string"
          },
          "impressionId": {
            "description": "Unique impression identifier",
            "type": "string"
          },
          "screenId": {
            "description": "Screen that displayed the ad",
            "type": "string"
          },
          "signature": {
            "description": "Cryptographic signature (format: ed25519=<hex> or sha256=<hex>)",
            "example": "ed25519=a1b2c3d4e5f6...",
            "type": "string"
          },
          "signatureVersion": {
            "description": "Signature version (v2 = Ed25519, v1 = HMAC)",
            "enum": [
              "v1",
              "v2"
            ],
            "type": "string"
          },
          "timestamp": {
            "description": "When the impression occurred",
            "format": "date-time",
            "type": "string"
          },
          "verifiedAt": {
            "description": "When the proof was generated",
            "format": "date-time",
            "type": "string"
          },
          "viewability": {
            "$ref": "#/components/schemas/Viewability"
          }
        },
        "required": [
          "impressionId",
          "adId",
          "timestamp",
          "signature",
          "signatureVersion"
        ],
        "type": "object"
      },
      "PublicKeyResponse": {
        "properties": {
          "algorithm": {
            "enum": [
              "ed25519"
            ],
            "type": "string"
          },
          "documentation": {
            "format": "uri",
            "type": "string"
          },
          "keyId": {
            "type": "string"
          },
          "publicKey": {
            "description": "Hex-encoded Ed25519 public key",
            "type": "string"
          },
          "success": {
            "type": "boolean"
          },
          "version": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "QrBeaconSignal": {
        "properties": {
          "canvasHash": {
            "type": "string"
          },
          "fingerprintId": {
            "type": "string"
          },
          "platform": {
            "type": "string"
          },
          "scanId": {
            "description": "The QR scan this signal belongs to.",
            "type": "string"
          },
          "screenHeight": {
            "type": "integer"
          },
          "screenWidth": {
            "type": "integer"
          },
          "timezone": {
            "type": "string"
          }
        },
        "required": [
          "scanId"
        ],
        "type": "object"
      },
      "RoasBreakdown": {
        "description": "Headline ROAS with attributed revenue by channel and creative.",
        "properties": {
          "byChannel": {
            "items": {
              "$ref": "#/components/schemas/RoasChannel"
            },
            "type": "array"
          },
          "byCreative": {
            "items": {
              "$ref": "#/components/schemas/RoasCreative"
            },
            "type": "array"
          },
          "overallROAS": {
            "description": "Total attributed revenue / total media cost; null when cost is unknown.",
            "nullable": true,
            "type": "number"
          },
          "totalAttributedRevenueCents": {
            "type": "integer"
          },
          "totalMediaCostCents": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "RoasChannel": {
        "properties": {
          "attributedCents": {
            "type": "integer"
          },
          "chains": {
            "description": "Number of attribution chains crediting this channel.",
            "type": "integer"
          },
          "channel": {
            "enum": [
              "dooh",
              "mobile",
              "web"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "RoasCreative": {
        "properties": {
          "attributedCents": {
            "type": "integer"
          },
          "avgAttentionScore": {
            "maximum": 1,
            "minimum": 0,
            "nullable": true,
            "type": "number"
          },
          "avgDwell": {
            "description": "Average dwell seconds across the creative's matched purchases.",
            "nullable": true,
            "type": "number"
          },
          "creativeId": {
            "type": "string"
          },
          "transactions": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "ScreenProofsResponse": {
        "properties": {
          "data": {
            "properties": {
              "generatedAt": {
                "format": "date-time",
                "type": "string"
              },
              "location": {
                "type": "object"
              },
              "pagination": {
                "$ref": "#/components/schemas/Pagination"
              },
              "proofs": {
                "items": {
                  "$ref": "#/components/schemas/Proof"
                },
                "type": "array"
              },
              "screenId": {
                "type": "string"
              },
              "screenName": {
                "type": "string"
              },
              "signatureVersion": {
                "type": "string"
              },
              "totalProofs": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "success": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "SegmentConsensus": {
        "description": "An audience segment confirmed by two or more demand partners.",
        "properties": {
          "partnerCount": {
            "description": "Distinct demand partners that included this segment.",
            "type": "integer"
          },
          "partners": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "segment": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "SignalQuality": {
        "description": "Signal quality and coverage metrics for a campaign. Use this to\nassess the reliability and completeness of attribution data.\n",
        "properties": {
          "campaign_id": {
            "type": "string"
          },
          "data_provenance": {
            "description": "Source of each data type for auditability",
            "properties": {
              "attention_source": {
                "description": "Source of attention/engagement signals",
                "type": "string"
              },
              "device_signal_source": {
                "description": "Provider of mobile device location signals",
                "type": "string"
              },
              "identity_graph_depth": {
                "description": "Maximum hop depth in identity resolution graph",
                "type": "integer"
              },
              "proof_of_play": {
                "description": "Cryptographic proof method for ad plays",
                "type": "string"
              }
            },
            "type": "object"
          },
          "measurement_confidence": {
            "properties": {
              "attention_weighted_avg_confidence": {
                "maximum": 1,
                "minimum": 0,
                "type": "number"
              },
              "bayesian_prob_positive": {
                "type": "number"
              },
              "exposure_matching_avg_confidence": {
                "maximum": 1,
                "minimum": 0,
                "type": "number"
              },
              "incrementality_p_value": {
                "type": "number"
              }
            },
            "type": "object"
          },
          "signal_coverage": {
            "description": "What percentage of impressions have each signal type",
            "properties": {
              "attention_coverage_pct": {
                "description": "Percentage of exposures with FEIN attention data",
                "type": "number"
              },
              "fein_matched_pct": {
                "description": "Percentage of exposures successfully joined with FEIN inference",
                "type": "number"
              },
              "footfall_measured_pct": {
                "description": "Percentage of exposed devices with footfall outcome data",
                "type": "number"
              },
              "identity_resolved_pct": {
                "description": "Percentage of devices resolved to household/cross-device graph",
                "type": "number"
              }
            },
            "type": "object"
          },
          "signal_freshness": {
            "description": "How fresh the signals are (real-time vs batch)",
            "properties": {
              "avg_signal_age_seconds": {
                "description": "Average age of device signals at time of ingestion",
                "type": "number"
              },
              "p95_signal_age_seconds": {
                "description": "95th percentile signal age",
                "type": "number"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "SignatureInfoResponse": {
        "properties": {
          "signatureInfo": {
            "properties": {
              "algorithm": {
                "type": "string"
              },
              "currentVersion": {
                "type": "string"
              },
              "documentation": {
                "type": "string"
              },
              "legacySupport": {
                "type": "object"
              },
              "payloadFormat": {
                "type": "string"
              },
              "publicKey": {
                "type": "string"
              },
              "publicKeyUrl": {
                "type": "string"
              },
              "signaturePrefix": {
                "type": "string"
              },
              "verificationEndpoint": {
                "type": "string"
              }
            },
            "type": "object"
          },
          "success": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "TransactionEventRequest": {
        "properties": {
          "advertiserId": {
            "type": "string"
          },
          "basketItems": {
            "items": {
              "properties": {
                "name": {
                  "type": "string"
                },
                "price_cents": {
                  "type": "integer"
                },
                "quantity": {
                  "type": "integer"
                },
                "sku": {
                  "type": "string"
                }
              },
              "type": "object"
            },
            "type": "array"
          },
          "campaignId": {
            "pattern": "^[a-zA-Z0-9_-]{1,128}$",
            "type": "string"
          },
          "maidHash": {
            "description": "SHA-256 hash of the mobile advertising ID",
            "type": "string"
          },
          "occurredAt": {
            "format": "date-time",
            "type": "string"
          },
          "poiId": {
            "description": "Point-of-interest where transaction occurred",
            "format": "uuid",
            "type": "string"
          },
          "transactionAmountCents": {
            "description": "Transaction amount in cents",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "TransactionSummary": {
        "description": "Purchase-matched attribution summary with ROAS and Bayesian sales lift.",
        "properties": {
          "attribution": {
            "properties": {
              "attributedRevenueCents": {
                "type": "integer"
              },
              "attributedTransactions": {
                "description": "Decay-weighted attributed transaction count.",
                "type": "number"
              },
              "iroas": {
                "description": "Incremental ROAS (incremental revenue / media cost); null when cost is unknown.",
                "nullable": true,
                "type": "number"
              },
              "roas": {
                "description": "attributedRevenueCents / totalMediaCostCents; null when cost is unknown.",
                "nullable": true,
                "type": "number"
              },
              "totalMediaCostCents": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "dateRange": {
            "properties": {
              "end": {
                "format": "date",
                "type": "string"
              },
              "start": {
                "format": "date",
                "type": "string"
              }
            },
            "type": "object"
          },
          "salesLift": {
            "properties": {
              "controlPurchasers": {
                "type": "integer"
              },
              "exposedPurchasers": {
                "type": "integer"
              },
              "incrementalRevenueCents": {
                "type": "integer"
              },
              "incrementalTransactions": {
                "type": "integer"
              },
              "posteriorProbPositive": {
                "description": "Bayesian P(sales lift > 0).",
                "maximum": 1,
                "minimum": 0,
                "nullable": true,
                "type": "number"
              },
              "salesLiftPct": {
                "description": "Relative lift in purchase rate, exposed vs control.",
                "nullable": true,
                "type": "number"
              }
            },
            "type": "object"
          },
          "timing": {
            "properties": {
              "avgHoursToPurchase": {
                "nullable": true,
                "type": "number"
              },
              "medianHoursToPurchase": {
                "nullable": true,
                "type": "number"
              }
            },
            "type": "object"
          },
          "transactions": {
            "properties": {
              "avgBasketCents": {
                "description": "Average basket value in cents.",
                "type": "integer"
              },
              "total": {
                "description": "Total matched transactions.",
                "type": "integer"
              },
              "totalRevenueCents": {
                "description": "Gross purchase revenue in cents.",
                "type": "integer"
              },
              "uniquePurchasers": {
                "type": "integer"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "TransactionTimeseriesPoint": {
        "properties": {
          "attributedRevenueCents": {
            "type": "integer"
          },
          "avgHoursToPurchase": {
            "nullable": true,
            "type": "number"
          },
          "date": {
            "format": "date",
            "type": "string"
          },
          "incrementalRevenueCents": {
            "type": "integer"
          },
          "incrementalTransactions": {
            "type": "integer"
          },
          "iroas": {
            "nullable": true,
            "type": "number"
          },
          "mediaCostCents": {
            "type": "integer"
          },
          "posteriorProbPositive": {
            "maximum": 1,
            "minimum": 0,
            "nullable": true,
            "type": "number"
          },
          "roas": {
            "nullable": true,
            "type": "number"
          },
          "salesLiftPct": {
            "nullable": true,
            "type": "number"
          },
          "totalRevenueCents": {
            "type": "integer"
          },
          "totalTransactions": {
            "type": "integer"
          },
          "uniquePurchasers": {
            "type": "integer"
          }
        },
        "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"
      },
      "VenueBidModifier": {
        "properties": {
          "bidModifierPct": {
            "description": "Bid modifier percentage, capped at [-80, +200].",
            "maximum": 200,
            "minimum": -80,
            "type": "number"
          },
          "confidence": {
            "enum": [
              "low",
              "medium",
              "high"
            ],
            "type": "string"
          },
          "conversionRate": {
            "type": "number"
          },
          "sampleSize": {
            "type": "integer"
          },
          "venueType": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "VenueLoyaltySummary": {
        "description": "Loyalty-tier breakdown of a campaign's exposed audience.",
        "properties": {
          "avgFrequency": {
            "description": "Average 90-day visit frequency.",
            "type": "number"
          },
          "avgLoyaltyScore": {
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "campaignId": {
            "type": "string"
          },
          "daypartBreakdown": {
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Device counts keyed by preferred daypart.",
            "type": "object"
          },
          "loyalDeviceCount": {
            "description": "Devices in the regular or loyalist tiers.",
            "type": "integer"
          },
          "tierBreakdown": {
            "additionalProperties": {
              "$ref": "#/components/schemas/LoyaltyTierStats"
            },
            "description": "Per-tier stats keyed by loyalty tier (drive_by, occasional, regular, loyalist).",
            "type": "object"
          },
          "topDaypart": {
            "nullable": true,
            "type": "string"
          },
          "totalDevices": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "VerifyProofRequest": {
        "properties": {
          "adId": {
            "type": "string"
          },
          "deviceId": {
            "type": "string"
          },
          "impressionId": {
            "type": "string"
          },
          "screenId": {
            "type": "string"
          },
          "signature": {
            "description": "Signature to verify (format: ed25519=<hex> or sha256=<hex>)",
            "type": "string"
          },
          "signatureVersion": {
            "description": "Optional explicit version (auto-detected from prefix)",
            "enum": [
              "v1",
              "v2"
            ],
            "type": "string"
          },
          "timestamp": {
            "description": "**Send epoch milliseconds.** The signed payload is built from the\nmillisecond value, so that is what has to be reproduced here.\n\nThe date-time string form below is accepted by the endpoint and\npassed through verbatim, so it rebuilds a different payload and the\nproof will not verify. Proofs returned by the campaign, screen and\nexport endpoints serialise their `timestamp` as ISO-8601 — convert\nit back to epoch milliseconds before sending it here.\n",
            "oneOf": [
              {
                "description": "Unix timestamp (milliseconds)",
                "type": "integer"
              },
              {
                "format": "date-time",
                "type": "string"
              }
            ]
          }
        },
        "required": [
          "signature",
          "timestamp"
        ],
        "type": "object"
      },
      "VerifyProofResponse": {
        "properties": {
          "publicKeyUrl": {
            "type": "string"
          },
          "success": {
            "type": "boolean"
          },
          "verification": {
            "properties": {
              "reason": {
                "description": "Reason for invalid signature (if applicable)",
                "type": "string"
              },
              "valid": {
                "description": "Whether the signature is valid",
                "type": "boolean"
              },
              "version": {
                "enum": [
                  "v1",
                  "v2"
                ],
                "type": "string"
              }
            },
            "type": "object"
          },
          "verifiedAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Viewability": {
        "properties": {
          "percentInView": {
            "description": "Percentage of ad visible (0-100)",
            "type": "number"
          },
          "viewable": {
            "description": "Whether the ad was viewable (IAB standards)",
            "type": "boolean"
          },
          "viewableTime": {
            "description": "Time the ad was viewable (milliseconds)",
            "type": "number"
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "bearerFormat": "JWT",
        "description": "JWT token obtained via POST /v1/user/login",
        "scheme": "bearer",
        "type": "http"
      },
      "bearerAuth__proof": {
        "bearerFormat": "JWT",
        "description": "JWT token from /v1/user/login",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "contact": {
      "email": "api@trillboards.com",
      "name": "Trillboards API Support",
      "url": "https://trillboards.com/support"
    },
    "description": "Attribution outcomes and cryptographically signed proof-of-play.\n\nEverything you can call with an **advertiser credential** to find out what a\ncampaign actually did: attribution and incrementality, attention metrics,\ncohort export, and the signed proof-of-play record for every delivered play.\n\nTwo previously separate references, merged.\n\nAll paths are absolute against `https://api.trillboards.com`.",
    "license": {
      "name": "Proprietary",
      "url": "https://trillboards.com/terms"
    },
    "title": "Trillboards Measurement API",
    "version": "1.0.0"
  },
  "openapi": "3.0.3",
  "paths": {
    "/v1/advertiser/attribution/campaign/{campaignId}": {
      "get": {
        "description": "Returns comprehensive attribution metrics for a campaign including reach,\nexposures, footfall visits, incremental lift, cost per visit, and statistical\nsignificance. This is the single endpoint that proves DOOH campaign ROI.\n\nThe response includes both raw metrics and quality indicators (match confidence,\ncontrol group size, significance level) so advertisers can assess data reliability.\n",
        "operationId": "getCampaignSummary",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          },
          {
            "description": "Attribution modes to include in the response. `dooh_only` (default)\nreturns the existing DOOH-only metrics. `cross_screen_tv` adds the\n`cross_screen_metrics` response field with TV-exposed-household reach\n+ tv_daypart_bucket lift. `cross_screen_all` is reserved for future\ncross-channel attribution (mobile + CTV streaming + DOOH + TV).\nMultiple values may be supplied; the response merges metrics for\nevery requested mode.\n",
            "in": "query",
            "name": "attributionModes",
            "required": false,
            "schema": {
              "default": [
                "dooh_only"
              ],
              "items": {
                "enum": [
                  "dooh_only",
                  "cross_screen_tv",
                  "cross_screen_all"
                ],
                "type": "string"
              },
              "type": "array"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "advertiser_id": "adv_xyz789",
                    "breakdowns": {
                      "daypart_breakdown": {
                        "evening": {
                          "lift_pct": 72,
                          "visitors": 1750
                        },
                        "morning": {
                          "lift_pct": 85,
                          "visitors": 1500
                        }
                      },
                      "venue_breakdown": {
                        "coffee_shop": {
                          "lift_pct": 65.3,
                          "visitors": 850
                        },
                        "gym": {
                          "lift_pct": 92.1,
                          "visitors": 1200
                        }
                      }
                    },
                    "campaign_id": "camp_abc123",
                    "cost": {
                      "cost_per_unique_reach": 0.22,
                      "total_media_cost": 10000
                    },
                    "date_range": {
                      "end": "2026-02-28",
                      "start": "2026-02-01"
                    },
                    "footfall": {
                      "control_visit_rate": 0.0403,
                      "control_visitors": 1820,
                      "cost_per_incremental_visit": 6.99,
                      "exposed_visit_rate": 0.0719,
                      "exposed_visitors": 3250,
                      "incremental_lift_pct": 78.4,
                      "incremental_visits": 1430
                    },
                    "quality": {
                      "attribution_window_days": 14,
                      "avg_match_confidence": 0.82,
                      "avg_time_to_visit_h": 48.3,
                      "control_group_size": 45200,
                      "statistical_significance": 0.997
                    },
                    "reach": {
                      "avg_frequency": 2.84,
                      "total_exposures": 128500,
                      "unique_devices_exposed": 45200
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AttributionSummary"
                    },
                    "success": {
                      "example": true,
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Attribution summary for the campaign"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Campaign attribution summary",
        "tags": [
          "Campaign Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/attention": {
      "get": {
        "description": "Returns attention-verified impression metrics from FEIN edge AI.\nThis is data that ONLY Trillboards can provide in DOOH.\n\nUnlike CTV viewability which uses probabilistic models (\"MRC standard\"\n= 50% of pixels visible for 2 seconds), Trillboards measures actual\nhuman attention via on-device computer vision:\n\n- **face_count**: How many people were actually in front of the screen\n- **attention_score**: What fraction were looking at the screen (gaze detection)\n- **dwell_seconds**: How long they stayed in view\n- **emotion_engagement**: Facial expression analysis (not recording, edge-only)\n- **aCPM**: Cost per 1,000 attention-verified impressions\n\nThese are MEASURED signals, not estimates.\n",
        "operationId": "getCampaignAttention",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attention": {
                      "attention_qualified_pct": 64.3,
                      "avg_attention_score": 0.72,
                      "avg_dwell_seconds": 8.4,
                      "avg_face_count": 2.3,
                      "median_attention_score": 0.68,
                      "p90_attention_score": 0.94
                    },
                    "breakdowns": {
                      "daypart_breakdown": {
                        "afternoon": {
                          "avg_attention": 0.65,
                          "avg_dwell": 7.1
                        },
                        "evening": {
                          "avg_attention": 0.74,
                          "avg_dwell": 8.8
                        },
                        "morning": {
                          "avg_attention": 0.78,
                          "avg_dwell": 9.2
                        }
                      },
                      "venue_type_breakdown": {
                        "coffee_shop": {
                          "avg_attention": 0.71,
                          "avg_dwell": 6.5
                        },
                        "gym": {
                          "avg_attention": 0.85,
                          "avg_dwell": 12.3
                        }
                      }
                    },
                    "campaign_id": "camp_abc123",
                    "economics": {
                      "attention_cpm": 19.4,
                      "cost_per_attentive_reach": 0.34,
                      "cost_per_attentive_visit": 8.75,
                      "standard_cpm": 12.5
                    },
                    "emotion": {
                      "avg_emotion_engagement": 0.58,
                      "emotion_breakdown": {
                        "happy": 28.3,
                        "interested": 8.4,
                        "neutral": 45.2,
                        "other": 4.3,
                        "surprised": 13.8
                      },
                      "positive_emotion_pct": 42.1
                    },
                    "total_impressions": 128500,
                    "unique_devices": 45200
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AttentionMetrics"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Attention metrics for the campaign"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Attention metrics summary",
        "tags": [
          "Attention Metrics"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/attention\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/attention/creative": {
      "get": {
        "description": "Compares attention metrics across creatives within a campaign.\nAnswers: \"Which creative gets more attention AND more store visits?\"\n\nThis is the holy grail of DOOH optimization — correlating creative\nexecution with actual human attention and downstream conversion.\nNo other platform can show this because no other platform measures\nattention at the creative level.\n",
        "operationId": "getCreativeAttentionComparison",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "attentionCpm": 7.5,
                      "attentionQualifiedPct": 75,
                      "avgAttentionScore": 0.85,
                      "avgDwellSeconds": 5.2,
                      "avgEmotionEngagement": 0.7,
                      "avgFaceCount": 2.5,
                      "creativeId": "creative-A",
                      "positiveEmotionPct": 60,
                      "totalImpressions": 5000,
                      "uniqueDevices": 1000
                    },
                    {
                      "attentionCpm": 12,
                      "attentionQualifiedPct": 52,
                      "avgAttentionScore": 0.55,
                      "avgDwellSeconds": 3.1,
                      "avgEmotionEngagement": 0.45,
                      "avgFaceCount": 1.8,
                      "creativeId": "creative-B",
                      "positiveEmotionPct": 40,
                      "totalImpressions": 3000,
                      "uniqueDevices": 800
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/CreativeAttentionComparison"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Per-creative attention comparison"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Per-creative attention comparison",
        "tags": [
          "Attention Metrics"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/attention/creative\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/attention/timeseries": {
      "get": {
        "description": "Returns daily attention quality metrics. Use this to detect\nattention fatigue: if attention_score drops over time while\nimpressions stay flat, the creative is wearing out and needs\nrefreshing.\n",
        "operationId": "getAttentionTimeseries",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          },
          {
            "in": "query",
            "name": "startDate",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "endDate",
            "schema": {
              "format": "date",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "attentionCpm": 8.33,
                      "attentionQualifiedPct": 68,
                      "avgAttentionScore": 0.72,
                      "avgDwellSeconds": 4.5,
                      "avgEmotionEngagement": 0.65,
                      "avgFaceCount": 2.1,
                      "date": "2026-03-01T00:00:00.000Z",
                      "standardCpm": 12.5,
                      "totalImpressions": 3000,
                      "uniqueDevices": 600
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/AttentionTimeseriesPoint"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Daily attention metrics timeseries"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Attention metrics over time",
        "tags": [
          "Attention Metrics"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/attention/timeseries\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/audience-overindex": {
      "get": {
        "description": "Returns how the campaign's measured audience skews versus the\nsurrounding neighborhood (Census ACS × FEIN edge-AI fusion). An\nover-index above 1.0 means a segment appears more often at your screens\nthan the local baseline; above 1.5 is a significant targeting signal.\n\nAnswers: \"Does this campaign actually over-deliver the audience I'm\nbuying?\" Returns `data: null` when no over-index has been computed yet.\n",
        "operationId": "getAudienceOverindex",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "ageOverindex": {
                      "18-24": 0.94,
                      "25-34": 1.82,
                      "35-44": 1.21
                    },
                    "campaignId": "camp_abc123",
                    "daysAnalyzed": 28,
                    "genderOverindex": {
                      "female": 0.88,
                      "male": 1.14
                    },
                    "screenCount": 18,
                    "significantSegments": [
                      {
                        "overindex": 1.82,
                        "segment": "25-34",
                        "type": "age"
                      }
                    ],
                    "topOverindexSegment": "25-34",
                    "topOverindexValue": 1.82
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/AudienceOverindex"
                        }
                      ],
                      "nullable": true,
                      "type": "object"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Aggregated over-index across the campaign's screens (null when not yet computed)"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Demographic over-index",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/audience-overindex\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/audience-profile": {
      "get": {
        "description": "Returns per-screen audience profiles built from the live bid stream:\nDSP-consensus audience segments (each confirmed by 2+ demand partners),\na demand score, and the top advertiser categories bidding on each venue.\n\nAnswers: \"Who does the open market think my venues reach, and how hot is\ndemand for them?\" These signals are inherited from bid intelligence at\nzero measurement cost.\n",
        "operationId": "getAudienceProfile",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "avgBidCpm": 6.42,
                      "bidPartnerCount": 7,
                      "createdAt": "2026-02-28T02:00:00Z",
                      "daypart": "morning",
                      "demandScore": 0.78,
                      "medianBidCpm": 5.9,
                      "profileDate": "2026-02-28",
                      "profileId": "550e8400-e29b-41d4-a716-446655440000",
                      "screenMongoId": "5f9a1b2c3d4e5f6a7b8c9d0e",
                      "segmentConsensus": [
                        {
                          "partnerCount": 4,
                          "partners": [
                            "dv360",
                            "thetradedesk",
                            "xandr",
                            "yahoo"
                          ],
                          "segment": "auto_intenders"
                        },
                        {
                          "partnerCount": 3,
                          "partners": [
                            "dv360",
                            "thetradedesk",
                            "xandr"
                          ],
                          "segment": "affluent_millennials"
                        }
                      ],
                      "topAdvertiserCategories": [
                        {
                          "category": "automotive",
                          "count": 820
                        },
                        {
                          "category": "qsr",
                          "count": 610
                        }
                      ],
                      "totalBids": 4200,
                      "updatedAt": "2026-02-28T02:00:00Z"
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/AudienceProfile"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Per-screen audience profiles"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "DSP-consensus audience profile",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/audience-profile\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/causal-lift": {
      "get": {
        "description": "Returns heterogeneous treatment-effect (causal lift) estimates broken\ndown by audience segment — age, gender, weather, daypart, brand\naffinity — computed daily via an X-learner model with bootstrap\nconfidence intervals.\n\nAnswers: \"The headline says my ads lifted visits — but WHICH audience\nactually moved?\" Surfaces the most- and least-responsive segments so\nbudget can shift toward the audiences the campaign genuinely persuades.\nComplements the aggregate chi-square result with the segment-level\nbreakdown. Every segment is returned with its exposed and control\ncounts.\n",
        "operationId": "getCampaignCausalLift",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          },
          {
            "description": "Measurement date (ISO 8601). Defaults to the most recent.",
            "in": "query",
            "name": "date",
            "required": false,
            "schema": {
              "format": "date",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "aggregateLift": {
                      "ciHigh": 0.167,
                      "ciLow": 0.081,
                      "controlN": 44180,
                      "exposedN": 45200,
                      "lift": 0.124
                    },
                    "campaignId": "camp_abc123",
                    "measuredDate": "2026-02-28",
                    "methodologyVersion": 2,
                    "perSegmentLifts": [
                      {
                        "ci_high": 0.284,
                        "ci_low": 0.142,
                        "control_n": 11980,
                        "exposed_n": 12400,
                        "lift": 0.213,
                        "segment": "age:25_34"
                      },
                      {
                        "ci_high": 0.087,
                        "ci_low": -0.011,
                        "control_n": 5940,
                        "exposed_n": 6100,
                        "lift": 0.038,
                        "segment": "weather:rain"
                      }
                    ],
                    "topResponsiveSegment": {
                      "ci_high": 0.284,
                      "ci_low": 0.142,
                      "lift": 0.213,
                      "segment": "age:25_34"
                    },
                    "topUnresponsiveSegment": {
                      "ci_high": 0.087,
                      "ci_low": -0.011,
                      "lift": 0.038,
                      "segment": "weather:rain"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CausalLiftReport"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Per-segment causal lift report"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Per-segment causal lift",
        "tags": [
          "Incrementality"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/causal-lift\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/channel-roas": {
      "get": {
        "description": "Returns return on ad spend (ROAS) broken down by channel.\nAnswers: \"DOOH vs mobile vs web — which channel pulls its weight?\"\n\nROAS = attributed_revenue / media_cost. A ROAS > 1.0 means the\nchannel generates more revenue than it costs. Use this to\njustify DOOH budget allocation vs digital channels.\n",
        "operationId": "getChannelROAS",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "avgTimeToConversionHours": 72.5,
                    "channelValue": {
                      "audio": 0,
                      "ctv": 0,
                      "display": 0,
                      "dooh": 1350000,
                      "mobile": 600000,
                      "olv": 0,
                      "store": 600000,
                      "web": 450000
                    },
                    "conversions": 80,
                    "totalChains": 100,
                    "totalConversionValue": 3000000
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ChannelROAS"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Per-channel ROAS"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Per-channel ROAS",
        "tags": [
          "Cross-Channel Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/channel-roas\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/cohort/export": {
      "post": {
        "description": "Exports the exposed audience cohort (SHA-256 hashed MAID list) to\na DSP for retargeting activation. Supported destinations:\n\n- **ttd**: The Trade Desk (1P data segment upload)\n- **dv360**: Google Display & Video 360 (Customer Match)\n- **meta**: Meta/Facebook (Custom Audiences)\n\nReturns a pre-signed S3 URL for the exported file and the MAID count.\n",
        "operationId": "exportCohort",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "destination": "ttd"
              },
              "schema": {
                "properties": {
                  "destination": {
                    "description": "Target DSP platform",
                    "enum": [
                      "ttd",
                      "dv360",
                      "meta"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "destination"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "exportId": "exp_a1b2c3d4",
                    "maidCount": 45200,
                    "s3Url": "https://trillboards-cohorts.s3.amazonaws.com/exports/camp_abc123/ttd/2026-02-28.csv?X-Amz-..."
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CohortExport"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Cohort exported successfully"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Export cohort to DSP",
        "tags": [
          "Cohort Export"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/cohort/export\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"destination\":\"ttd\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/cohort/refresh": {
      "post": {
        "description": "Forces an immediate re-sync of the campaign's exposed cohort to a DSP\ndestination. The raw encrypted-identifier cohort is routed through\nIntuizi, which resolves it and fans out to the requested DSP on their\nactivation graph.\n\nAnswers: \"Push my freshest exposed audience to the DSP now — don't wait\nfor the daily refresh.\" Returns 404 when no ready cohort exists yet.\n",
        "operationId": "refreshCohort",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "destination": "ttd"
              },
              "schema": {
                "$ref": "#/components/schemas/CohortRefreshRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "destination": "ttd",
                    "durationMs": 1840,
                    "maidCount": 45200,
                    "routedVia": "intuizi",
                    "s3Url": "https://trillboards-cohorts.s3.amazonaws.com/exports/camp_abc123/ttd/2026-02-28.csv?X-Amz-...",
                    "segmentId": "550e8400-e29b-41d4-a716-446655440000",
                    "status": "activated"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CohortRefreshResult"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Cohort re-sync accepted and routed to Intuizi"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Force cohort re-sync to a DSP",
        "tags": [
          "Cohort Export"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/cohort/refresh\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"destination\":\"ttd\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/creative-attribution": {
      "get": {
        "description": "Returns attribution outcomes broken down by creative. Correlates\ncreative execution (attention, dwell, emotion) with downstream\noutcomes (store visits, lift, cost per visit).\n\nIncludes context interaction analysis: which creative performs best\nin which venue type, daypart, and weather condition.\n",
        "operationId": "getCreativeAttribution",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "attention": {
                        "avgDwell": 5.5,
                        "avgEmotion": 0.7,
                        "avgScore": 0.82,
                        "dominantEmotion": "happy"
                      },
                      "avgCostPerVisit": 4.25,
                      "avgLiftPct": 15.5,
                      "avgVisitRate": 0.012,
                      "bestContext": {
                        "daypart": "morning",
                        "venueType": "gas_station",
                        "weather": "sunny"
                      },
                      "creativeId": "creative-A",
                      "dateRange": {
                        "daysMeasured": 5,
                        "first": "2026-03-01T00:00:00.000Z",
                        "last": "2026-03-05T00:00:00.000Z"
                      },
                      "totalVisits": 200,
                      "variant": "default"
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/CreativeAttributionPerformance"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Creative attribution performance data"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Creative-level attribution performance",
        "tags": [
          "Creative Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/creative-attribution\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/creative-attribution/{creativeId}/timeseries": {
      "get": {
        "description": "Returns daily attribution metrics for a specific creative.\nUse this to detect creative fatigue: if attention degrades while\nimpressions stay constant, rotate the creative.\n",
        "operationId": "getCreativeAttributionTimeseries",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          },
          {
            "description": "Creative identifier",
            "in": "path",
            "name": "creativeId",
            "required": true,
            "schema": {
              "pattern": "^[a-zA-Z0-9_-]{1,128}$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "avgAttention": 0.82,
                      "avgDwell": 5.5,
                      "avgEmotion": 0.7,
                      "bestDaypart": "morning",
                      "bestVenue": "gas_station",
                      "bestWeather": "sunny",
                      "costPerVisit": 4.25,
                      "date": "2026-03-01T00:00:00.000Z",
                      "liftPct": 15.5,
                      "storeVisits": 40,
                      "visitRate": 0.012
                    },
                    {
                      "avgAttention": 0.79,
                      "avgDwell": 5.1,
                      "avgEmotion": 0.68,
                      "bestDaypart": "evening",
                      "bestVenue": "gas_station",
                      "bestWeather": "cloudy",
                      "costPerVisit": null,
                      "date": "2026-03-02T00:00:00.000Z",
                      "liftPct": null,
                      "storeVisits": 36,
                      "visitRate": 0.0109
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/CreativeAttributionTimeseriesPoint"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Creative timeseries data"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Creative attribution over time",
        "tags": [
          "Creative Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/creative-attribution/{creativeId}/timeseries\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/cross-channel-journey": {
      "get": {
        "description": "Returns flow data suitable for Sankey diagrams showing how users\nmove across channels after DOOH exposure.\n\nExample flow: Billboard (42%) -> Mobile Ad Click (28%) -> Website Visit (18%) -> Store Purchase (12%)\n\nEach node/link includes volume and conversion rate, enabling\nvisualization of the most common and most valuable paths.\n",
        "operationId": "getCrossChannelJourney",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "channels": [
                      {
                        "channel": "dooh",
                        "touchpoints": 500,
                        "uniqueDevices": 200
                      },
                      {
                        "channel": "mobile_ad",
                        "touchpoints": 300,
                        "uniqueDevices": 150
                      },
                      {
                        "channel": "store_visit",
                        "touchpoints": 100,
                        "uniqueDevices": 80
                      }
                    ],
                    "flows": [
                      {
                        "count": 50,
                        "source": "dooh",
                        "target": "mobile_ad"
                      },
                      {
                        "count": 30,
                        "source": "mobile_ad",
                        "target": "store_visit"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CrossChannelJourney"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Sankey flow data"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Cross-channel journey (Sankey flow)",
        "tags": [
          "Cross-Channel Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/cross-channel-journey\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/downtime-experiments": {
      "get": {
        "description": "Screen-downtime natural experiments are NOT MEASURED. They took the\ndevices seen near a dark screen as a control group and conversion\nvisits as the outcome; the control group came from a PostgreSQL copy\nthat stopped taking writes, and conversion visits have no store. No\nexperiment was ever produced, so no published number is withdrawn.\n\nAlways answers zero experiments with no `avgLiftPct` (an average over\nno experiments is omitted, never reported as 0), and an `outcome` of\nkind `not_measured` saying why.\n",
        "operationId": "getDowntimeExperiments",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "experiments": [],
                    "significantExperiments": 0,
                    "totalExperiments": 0
                  },
                  "outcome": {
                    "kind": "not_measured",
                    "reason": "inputs_frozen_in_postgres_no_conversion_visits",
                    "what": "downtime_experiments"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/DowntimeExperiments"
                    },
                    "outcome": {
                      "$ref": "#/components/schemas/NotMeasuredOutcome"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Zero experiments; `outcome` says why they are not measured"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Screen-downtime natural experiments (not measured)",
        "tags": [
          "Incrementality"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/downtime-experiments\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/heatmap": {
      "get": {
        "description": "Returns geospatial clusters of exposure and conversion density.\nAnswers: \"WHERE are people seeing my ads and then visiting my stores?\"\n\nEach point represents a geographic cluster with exposure count,\nconversion count, and density. Use for map visualizations showing\ncampaign impact by location.\n",
        "operationId": "getCampaignHeatmap",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "conversion_count": 245,
                      "density": 0.89,
                      "exposure_count": 3200,
                      "geohash": "dr5ru7",
                      "latitude": 40.7128,
                      "longitude": -74.006
                    },
                    {
                      "conversion_count": 198,
                      "density": 0.76,
                      "exposure_count": 2800,
                      "geohash": "dr5rub",
                      "latitude": 40.758,
                      "longitude": -73.9855
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/HeatmapPoint"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Geospatial heatmap data"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Geospatial exposure heatmap",
        "tags": [
          "Campaign Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/heatmap\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/incrementality": {
      "get": {
        "description": "Returns Bayesian uplift measurement for a campaign. This is the\ngold standard of DOOH attribution — proving that store visits\nWOULD NOT have happened without the campaign.\n\nResults include:\n- **Bayesian posterior**: P(lift > 0) and 95% credible intervals\n- **Frequentist**: chi-square, p-value, confidence intervals\n- **Propensity quality**: standardized mean difference for covariate balance\n\nA P(lift > 0) > 0.95 indicates strong evidence of causal effect.\n",
        "operationId": "getCampaignIncrementality",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "controlDmas": [
                        "504",
                        "623",
                        "819"
                      ],
                      "experimentId": "550e8400-e29b-41d4-a716-446655440000",
                      "latestResult": {
                        "control": {
                          "devices": 47850,
                          "rate": 0.02178,
                          "visitors": 1042
                        },
                        "credibleInterval": {
                          "lower": 0.0004,
                          "upper": 0.0061
                        },
                        "date": "2026-03-05T00:00:00.000Z",
                        "expectedUplift": 0.0032,
                        "incrementalVisits": 162,
                        "lift": 14.7,
                        "pValue": 0.031,
                        "posteriorProbPositive": 0.968,
                        "treatment": {
                          "devices": 48200,
                          "rate": 0.02498,
                          "visitors": 1204
                        }
                      },
                      "status": "measuring",
                      "treatmentDmas": [
                        "501",
                        "602",
                        "803"
                      ],
                      "type": "geo_holdout"
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "description": "One entry per experiment on the campaign, newest `created_at`\nfirst. An EMPTY ARRAY when the campaign has no experiments —\nthat is the normal no-experiment answer, not a 404.\n",
                      "items": {
                        "$ref": "#/components/schemas/IncrementalityResult"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Incrementality results"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Incrementality results",
        "tags": [
          "Incrementality"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/incrementality\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/journey": {
      "get": {
        "description": "Returns the attribution funnel showing how exposed devices progress\nthrough the conversion journey: Exposure -> Visit -> Transaction.\n\nUse this to understand conversion rates at each stage and identify\nwhere the funnel leaks.\n",
        "operationId": "getCampaignJourney",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "avgTimeToVisitHours": 72.5,
                    "funnel": {
                      "impressions": 10000,
                      "uniqueExposed": 500,
                      "visited14d": 60,
                      "visited7d": 45
                    },
                    "medianTimeToVisitHours": 48
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/JourneyFunnel"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Journey funnel data"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Attribution journey funnel",
        "tags": [
          "Campaign Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/journey\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/methodology": {
      "get": {
        "description": "Returns a human-readable explanation of how attribution was computed\nfor THIS specific campaign. Covers exposure matching methodology,\nattention enrichment, incrementality measurement approach, and\ncross-channel attribution model.\n\nThis is RADICAL TRANSPARENCY — no other DOOH platform explains\ntheir methodology at the campaign level.\n",
        "operationId": "getMethodology",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attention_enrichment": {
                      "description": "Joined exposure matches with FEIN edge AI inference outcomes (face detection, gaze tracking, emotion analysis) to compute attention-weighted confidence scores.",
                      "method": "FEIN edge AI inference join",
                      "signals": [
                        "face_count",
                        "attention_score",
                        "dwell_seconds",
                        "emotion_score",
                        "engagement_score"
                      ]
                    },
                    "campaign_id": "camp_abc123",
                    "cross_channel": {
                      "channels": [
                        "dooh",
                        "mobile_ad",
                        "web_visit",
                        "store_visit"
                      ],
                      "description": "Time-decay attribution with 7-day half-life across 4 channels (DOOH, mobile, web, store). Credit allocated proportionally to recency and attention weight.",
                      "half_life_hours": 168,
                      "method": "Time-decay multi-touch attribution",
                      "model": "time_decay"
                    },
                    "exposure_matching": {
                      "confidence_model": "Inverse distance + time decay",
                      "description": "Matched mobile device location signals within 200m of screens during ad play windows using PostGIS ST_DWithin. Distance and time delta are recorded for each match.",
                      "max_distance_m": 200,
                      "method": "PostGIS ST_DWithin geospatial join"
                    },
                    "incrementality": {
                      "control_group_matching": "DMA-level propensity score matching",
                      "description": "Geo-holdout experiment with DMA-matched control group. Bayesian Beta-Binomial model with 10,000 Monte Carlo samples. Frequentist chi-square test for comparison.",
                      "experiment_type": "geo_holdout",
                      "method": "Bayesian Beta-Binomial uplift model"
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Methodology"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Methodology explanation"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Attribution methodology explanation",
        "tags": [
          "Signal Transparency"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/methodology\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/multi-touch": {
      "get": {
        "description": "Returns cross-channel credit allocation across DOOH, mobile, web,\nand store touchpoints. Supports multiple attribution models:\n\n- **time_decay**: Recent touchpoints get more credit (7-day half-life default)\n- **position_based**: First and last touch get 40% each, middle touches share 20%\n- **attention_weighted**: Credit proportional to FEIN attention score at each touchpoint\n- **shapley**: Game-theory-based fair value allocation (computationally intensive)\n\nThis answers: \"How much credit does DOOH deserve vs mobile vs web?\"\n",
        "operationId": "getMultiTouchAttribution",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "campaign_id": "camp_abc123",
                    "channel_credits": {
                      "dooh": {
                        "attributed_conversions": 608,
                        "attributed_revenue_cents": 911625,
                        "credit_pct": 42.5
                      },
                      "mobile_ad": {
                        "attributed_conversions": 405,
                        "attributed_revenue_cents": 607035,
                        "credit_pct": 28.3
                      },
                      "store_visit": {
                        "attributed_conversions": 150,
                        "attributed_revenue_cents": 225225,
                        "credit_pct": 10.5
                      },
                      "web_visit": {
                        "attributed_conversions": 267,
                        "attributed_revenue_cents": 401115,
                        "credit_pct": 18.7
                      }
                    },
                    "model": "time_decay",
                    "total_attributed_revenue_cents": 2145000,
                    "total_conversions": 1430
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/MultiTouchAttribution"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Multi-touch attribution data"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Multi-touch attribution summary",
        "tags": [
          "Cross-Channel Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/multi-touch\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/optimizations": {
      "get": {
        "description": "Returns the current creative impression-share weights and venue/daypart\nbid modifiers computed from attribution outcomes, plus a human-readable\nexplanation and the campaign's auto-optimization settings.\n\nAnswers: \"Given what actually converted, where should my impressions and\nbids go?\" Creative weights are proportional to visit-rate × attention\nwith an exploration floor so no creative is fully suppressed; modifiers\nare capped at [-80%, +200%].\n",
        "operationId": "getOptimizations",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "autoOptimize": true,
                    "campaignId": "camp_abc123",
                    "computedAt": "2026-02-28T13:30:00Z",
                    "creativeWeights": [
                      {
                        "attentionScore": 0.74,
                        "confidence": "high",
                        "creativeId": "cre_x1",
                        "totalImpressions": 82000,
                        "totalVisits": 5820,
                        "visitRate": 0.071,
                        "weight": 0.62
                      },
                      {
                        "attentionScore": 0.61,
                        "confidence": "medium",
                        "creativeId": "cre_x2",
                        "totalImpressions": 61000,
                        "totalVisits": 3170,
                        "visitRate": 0.052,
                        "weight": 0.38
                      }
                    ],
                    "daypartModifiers": [
                      {
                        "bidModifierPct": 45,
                        "confidence": "high",
                        "conversionRate": 0.066,
                        "daypart": "morning",
                        "sampleSize": 18200
                      }
                    ],
                    "explanation": "Creative cre_x1 receives 62% of impressions because it drives a 7.1% visit rate with 0.7s avg attention.",
                    "explorationWeight": 0.1,
                    "minSampleSize": 100,
                    "venueModifiers": [
                      {
                        "bidModifierPct": 92.1,
                        "confidence": "high",
                        "conversionRate": 0.081,
                        "sampleSize": 12400,
                        "venueType": "gym"
                      }
                    ]
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OptimizationReport"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Optimization weights and modifiers"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Attribution-driven optimization weights",
        "tags": [
          "Optimization"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/optimizations\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/optimizations/auto": {
      "put": {
        "description": "Enables or disables automatic attribution-driven optimization for a\ncampaign, and optionally tunes the minimum sample size and exploration\nweight. When enabled, the nightly job recomputes creative weights and\nvenue/daypart bid modifiers from fresh attribution outcomes.\n\nAnswers: \"Let the attribution loop steer my impressions and bids\nautomatically.\"\n",
        "operationId": "setAutoOptimize",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "enabled": true,
                "explorationWeight": 0.1,
                "minSampleSize": 100
              },
              "schema": {
                "$ref": "#/components/schemas/AutoOptimizeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "autoOptimize": true,
                    "campaignId": "camp_abc123",
                    "explorationWeight": 0.1,
                    "minSampleSize": 100
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AutoOptimizeConfig"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Updated auto-optimization configuration"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Toggle auto-optimization",
        "tags": [
          "Optimization"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X PUT \\\n  \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/optimizations/auto\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"enabled\":true,\"minSampleSize\":100,\"explorationWeight\":0.1}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/roas": {
      "get": {
        "description": "Returns headline ROAS with attributed revenue broken down by channel\n(DOOH, mobile, web) and by creative. Answers: \"Which channel and which\ncreative actually returned the spend?\"\n",
        "operationId": "getRoasBreakdown",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "byChannel": [
                      {
                        "attributedCents": 1298400,
                        "chains": 412,
                        "channel": "dooh"
                      },
                      {
                        "attributedCents": 906800,
                        "chains": 288,
                        "channel": "mobile"
                      },
                      {
                        "attributedCents": 849600,
                        "chains": 205,
                        "channel": "web"
                      }
                    ],
                    "byCreative": [
                      {
                        "attributedCents": 1120400,
                        "avgAttentionScore": 0.71,
                        "avgDwell": 8.4,
                        "creativeId": "cre_x1",
                        "transactions": 214
                      },
                      {
                        "attributedCents": 894200,
                        "avgAttentionScore": 0.63,
                        "avgDwell": 6.9,
                        "creativeId": "cre_x2",
                        "transactions": 168
                      }
                    ],
                    "overallROAS": 3.05,
                    "totalAttributedRevenueCents": 3054800,
                    "totalMediaCostCents": 1000000
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/RoasBreakdown"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "ROAS breakdown"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "ROAS breakdown by channel and creative",
        "tags": [
          "Transaction & ROAS"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/roas\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/signal-quality": {
      "get": {
        "description": "Returns signal quality metrics showing how complete and fresh the\nattribution data is for this campaign. This is radical transparency —\nno other DOOH platform exposes their data quality at the campaign level.\n\nUse this to assess confidence in attribution results:\n- High attention_coverage_pct = most impressions have FEIN verification\n- Low avg_signal_age_seconds = signals are fresh (real-time, not batch)\n- High bayesian_prob_positive = strong evidence of causal effect\n",
        "operationId": "getSignalQuality",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "campaign_id": "camp_abc123",
                    "data_provenance": {
                      "attention_source": "fein_edge_ai",
                      "device_signal_source": "intuizi",
                      "identity_graph_depth": 2,
                      "proof_of_play": "ed25519_signed"
                    },
                    "measurement_confidence": {
                      "attention_weighted_avg_confidence": 0.91,
                      "bayesian_prob_positive": 0.997,
                      "exposure_matching_avg_confidence": 0.82,
                      "incrementality_p_value": 0.003
                    },
                    "signal_coverage": {
                      "attention_coverage_pct": 87.3,
                      "fein_matched_pct": 92.1,
                      "footfall_measured_pct": 68.2,
                      "identity_resolved_pct": 74.5
                    },
                    "signal_freshness": {
                      "avg_signal_age_seconds": 18,
                      "p95_signal_age_seconds": 45
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/SignalQuality"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Signal quality report"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Signal quality and coverage",
        "tags": [
          "Signal Transparency"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/signal-quality\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/timeseries": {
      "get": {
        "description": "Returns daily attribution metrics over time. Use this to answer:\n\"Is my campaign's lift improving or degrading?\" and \"Which days drive\nthe most store visits?\"\n\nSupports date range filtering. If no dates specified, returns the full\ncampaign duration.\n",
        "operationId": "getCampaignTimeseries",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          },
          {
            "description": "Start date (ISO 8601, e.g. 2026-02-01)",
            "in": "query",
            "name": "startDate",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "End date (ISO 8601, e.g. 2026-02-28)",
            "in": "query",
            "name": "endDate",
            "schema": {
              "format": "date",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "avg_match_confidence": 0.83,
                      "control_visitors": 65,
                      "cost_per_incremental_visit": 7.84,
                      "exposed_visitors": 116,
                      "incremental_lift_pct": 78.5,
                      "incremental_visits": 51,
                      "statistical_significance": 0.982,
                      "summary_date": "2026-02-15",
                      "total_exposures": 4580,
                      "unique_devices_exposed": 1620
                    },
                    {
                      "avg_match_confidence": 0.85,
                      "control_visitors": 72,
                      "cost_per_incremental_visit": 6.45,
                      "exposed_visitors": 134,
                      "incremental_lift_pct": 86.1,
                      "incremental_visits": 62,
                      "statistical_significance": 0.994,
                      "summary_date": "2026-02-16",
                      "total_exposures": 5120,
                      "unique_devices_exposed": 1780
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/AttributionTimeseriesPoint"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Daily timeseries of attribution metrics"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Daily attribution timeseries",
        "tags": [
          "Campaign Attribution"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/timeseries\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/transactions": {
      "get": {
        "description": "Returns purchase-matched attribution for a campaign: total and\nattributed transactions, attributed revenue, ROAS, iROAS, Bayesian\nsales lift (exposed vs control), and time-to-purchase timing.\n\nAnswers: \"Did the people who saw my ad actually buy — and was the media\nspend worth it?\" Transactions are ingested from your POS or\nserver-postback conversion source and matched to DOOH exposures within\na 14-day window using time-decay weighting.\n",
        "operationId": "getTransactionAttribution",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "attribution": {
                      "attributedRevenueCents": 3054800,
                      "attributedTransactions": 612.4,
                      "iroas": 1.42,
                      "roas": 3.05,
                      "totalMediaCostCents": 1000000
                    },
                    "dateRange": {
                      "end": "2026-02-28",
                      "start": "2026-02-01"
                    },
                    "salesLift": {
                      "controlPurchasers": 540,
                      "exposedPurchasers": 980,
                      "incrementalRevenueCents": 1420000,
                      "incrementalTransactions": 430,
                      "posteriorProbPositive": 0.994,
                      "salesLiftPct": 62.4
                    },
                    "timing": {
                      "avgHoursToPurchase": 41.2,
                      "medianHoursToPurchase": 28.5
                    },
                    "transactions": {
                      "avgBasketCents": 4990,
                      "total": 1840,
                      "totalRevenueCents": 9182400,
                      "uniquePurchasers": 1620
                    }
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TransactionSummary"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Transaction attribution summary"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Transaction attribution & ROAS summary",
        "tags": [
          "Transaction & ROAS"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/transactions\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/transactions/timeseries": {
      "get": {
        "description": "Returns daily transaction attribution and ROAS across the campaign.\nAnswers: \"Which days actually converted, and is my ROAS trending up or\ndown?\" Supports optional date-range filtering.\n",
        "operationId": "getTransactionTimeseries",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          },
          {
            "description": "Start date (ISO 8601, e.g. 2026-02-01)",
            "in": "query",
            "name": "startDate",
            "schema": {
              "format": "date",
              "type": "string"
            }
          },
          {
            "description": "End date (ISO 8601, e.g. 2026-02-28)",
            "in": "query",
            "name": "endDate",
            "schema": {
              "format": "date",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "attributedRevenueCents": 106400,
                      "avgHoursToPurchase": 39.7,
                      "date": "2026-02-15",
                      "incrementalRevenueCents": 46800,
                      "incrementalTransactions": 15,
                      "iroas": 1.31,
                      "mediaCostCents": 35700,
                      "posteriorProbPositive": 0.981,
                      "roas": 2.98,
                      "salesLiftPct": 58.2,
                      "totalRevenueCents": 319360,
                      "totalTransactions": 64,
                      "uniquePurchasers": 58
                    }
                  ],
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "$ref": "#/components/schemas/TransactionTimeseriesPoint"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Daily ROAS timeseries"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Daily ROAS timeseries",
        "tags": [
          "Transaction & ROAS"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/transactions/timeseries\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/venue-loyalty": {
      "get": {
        "description": "Returns the loyalty-tier breakdown of the exposed audience — drive-by,\noccasional, regular, and loyalist — with average visit frequency,\nloyal-device counts, and the preferred daypart.\n\nAnswers: \"Am I reaching one-time passers-by or the regulars who define\nthis venue?\" Loyalty is derived from repeat-exposure patterns over a\n90-day window.\n",
        "operationId": "getVenueLoyalty",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "avgFrequency": 4.8,
                    "avgLoyaltyScore": 0.52,
                    "campaignId": "camp_abc123",
                    "daypartBreakdown": {
                      "afternoon": 12400,
                      "evening": 9100,
                      "morning": 19800,
                      "night": 3900
                    },
                    "loyalDeviceCount": 13400,
                    "tierBreakdown": {
                      "drive_by": {
                        "avgFrequency": 1,
                        "avgLoyaltyScore": 0.12,
                        "deviceCount": 18200
                      },
                      "loyalist": {
                        "avgFrequency": 14.6,
                        "avgLoyaltyScore": 0.87,
                        "deviceCount": 4300
                      },
                      "occasional": {
                        "avgFrequency": 2.9,
                        "avgLoyaltyScore": 0.38,
                        "deviceCount": 13600
                      },
                      "regular": {
                        "avgFrequency": 7.2,
                        "avgLoyaltyScore": 0.63,
                        "deviceCount": 9100
                      }
                    },
                    "topDaypart": "morning",
                    "totalDevices": 45200
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/VenueLoyaltySummary"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Venue loyalty summary"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Venue loyalty breakdown",
        "tags": [
          "Audience Intelligence"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/venue-loyalty\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/campaign/{campaignId}/weather-incrementality": {
      "get": {
        "description": "Weather-as-natural-experiment lift is NOT MEASURED. It compared visit\nrates on good- vs bad-weather days per DMA, and both of its inputs (the\nDMA split and the per-screen weather) were PostgreSQL copies that\nstopped taking writes, with no ClickHouse equivalent to join. It never\nproduced a result, so no published number is withdrawn.\n\nAlways answers `data: null` with an `outcome` of kind `not_measured`\nsaying why. `data: null` here does not mean \"not computed yet\": no\nanalysis runs.\n",
        "operationId": "getWeatherIncrementality",
        "parameters": [
          {
            "$ref": "#/components/parameters/campaignId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": null,
                  "outcome": {
                    "kind": "not_measured",
                    "reason": "inputs_frozen_in_postgres_no_clickhouse_join",
                    "what": "weather_incrementality"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "description": "Always null — nothing measures this quantity.",
                      "nullable": true,
                      "type": "object"
                    },
                    "outcome": {
                      "$ref": "#/components/schemas/NotMeasuredOutcome"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "No weather incrementality; `outcome` says why it is not measured"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Weather-based incrementality (not measured)",
        "tags": [
          "Incrementality"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/campaign/{campaignId}/weather-incrementality\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/experiment": {
      "post": {
        "description": "Creates a new incrementality experiment with specified design:\n\n- **geo_holdout**: Withhold ads in control DMAs, compare visit rates\n- **ghost_ad**: Serve PSA ads to control group, measure true lift\n- **psa_control**: Serve public service announcements as control creatives\n- **matched_market**: Match treatment/control markets on covariates\n\nThe experiment runs automatically once created. Results populate\nin the experiment status endpoint as data accumulates.\n",
        "operationId": "createExperiment",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "campaignId": "camp_abc123",
                "controlDmas": [
                  "504",
                  "623",
                  "819"
                ],
                "experimentType": "geo_holdout",
                "holdoutPct": 0.1,
                "targetAlpha": 0.05,
                "targetMde": 0.1,
                "targetPower": 0.8,
                "treatmentDmas": [
                  "501",
                  "602",
                  "803"
                ]
              },
              "schema": {
                "$ref": "#/components/schemas/CreateExperimentRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "advertiser_id": "66f0a1b2c3d4e5f607182930",
                    "caliper_width": 0.25,
                    "campaign_id": "camp_abc123",
                    "control_dmas": [
                      "504",
                      "623",
                      "819"
                    ],
                    "created_at": "2026-03-01T09:15:00.000Z",
                    "ended_at": null,
                    "experiment_id": "550e8400-e29b-41d4-a716-446655440000",
                    "experiment_type": "geo_holdout",
                    "holdout_pct": 0.1,
                    "matching_covariates": {},
                    "required_sample_size": null,
                    "started_at": null,
                    "status": "draft",
                    "target_alpha": 0.05,
                    "target_mde": 0.1,
                    "target_power": 0.8,
                    "treatment_dmas": [
                      "501",
                      "602",
                      "803"
                    ],
                    "updated_at": "2026-03-01T09:15:00.000Z"
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Experiment"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Experiment created"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create incrementality experiment",
        "tags": [
          "Incrementality"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/advertiser/attribution/experiment\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"campaignId\":\"camp_abc123\",\"experimentType\":\"geo_holdout\",\"treatmentDmas\":[\"501\",\"602\",\"803\"],\"controlDmas\":[\"504\",\"623\",\"819\"],\"holdoutPct\":0.1,\"targetMde\":0.1,\"targetPower\":0.8,\"targetAlpha\":0.05}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/experiment/{experimentId}": {
      "get": {
        "description": "Returns experiment configuration, status, and results (if available).\nResults include both Bayesian posteriors and frequentist p-values.\n",
        "operationId": "getExperiment",
        "parameters": [
          {
            "$ref": "#/components/parameters/experimentId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "experiment": {
                      "advertiser_id": "66f0a1b2c3d4e5f607182930",
                      "caliper_width": 0.25,
                      "campaign_id": "camp_abc123",
                      "control_dmas": [
                        "504",
                        "623",
                        "819"
                      ],
                      "created_at": "2026-03-01T09:15:00.000Z",
                      "ended_at": null,
                      "experiment_id": "550e8400-e29b-41d4-a716-446655440000",
                      "experiment_type": "geo_holdout",
                      "holdout_pct": 0.1,
                      "matching_covariates": {},
                      "required_sample_size": null,
                      "started_at": "2026-03-02T00:00:00.000Z",
                      "status": "measuring",
                      "target_alpha": 0.05,
                      "target_mde": 0.1,
                      "target_power": 0.8,
                      "treatment_dmas": [
                        "501",
                        "602",
                        "803"
                      ],
                      "updated_at": "2026-03-02T00:00:00.000Z"
                    },
                    "latestResult": {
                      "chi_square": 4.65,
                      "confidence_interval_lower": 0.0004,
                      "confidence_interval_upper": 0.0061,
                      "control_devices": 47850,
                      "control_visit_rate": 0.02178,
                      "control_visitors": 1042,
                      "expected_uplift": 0.0032,
                      "experiment_id": "550e8400-e29b-41d4-a716-446655440000",
                      "incremental_visits": 162,
                      "measurement_date": "2026-03-05T00:00:00.000Z",
                      "p_value": 0.031,
                      "posterior_prob_positive": 0.968,
                      "relative_lift_pct": 14.7,
                      "result_id": "4821",
                      "treatment_devices": 48200,
                      "treatment_visit_rate": 0.02498,
                      "treatment_visitors": 1204
                    },
                    "resultHistory": []
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ExperimentWithResults"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Experiment details with results"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Experiment status and results",
        "tags": [
          "Incrementality"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/experiment/{experimentId}\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/attribution/experiment/{experimentId}/power": {
      "get": {
        "description": "Returns the required sample size to detect a given minimum detectable\neffect (MDE) at specified significance and power levels.\n\nUse this before running an experiment to ensure you'll have enough\ndata to detect the effect you care about.\n",
        "operationId": "getExperimentPower",
        "parameters": [
          {
            "$ref": "#/components/parameters/experimentId"
          },
          {
            "description": "Baseline visit rate (default 2%)",
            "in": "query",
            "name": "baselineRate",
            "schema": {
              "default": 0.02,
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "alpha": 0.05,
                    "baselineRate": 0.02,
                    "mde": 0.1,
                    "power": 0.8,
                    "required_sample_per_group": 3842,
                    "total_sample_needed": 7684
                  },
                  "success": true
                },
                "schema": {
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PowerAnalysis"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Power analysis results"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Power analysis",
        "tags": [
          "Incrementality"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/advertiser/attribution/experiment/{experimentId}/power\" \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/conversion-source": {
      "post": {
        "description": "Creates a measurement connection for the authenticated advertiser.\n\nThe response is ONE OF ONLY TWO places the full `webhook_secret` ever\nappears (the other is the roll). Every later read redacts it to\n`webhook_secret_last4`. A client-supplied secret is ignored by contract\n— it is generated server-side.\n\n`advertiser_mongo_id` is always the JWT owner and is never taken from\nthe body.\n\n**This create can turn an existing connection OFF.** One active source\nper (advertiser, `source_type`, `domain`) is the model, so a create\nREPLACES. An `mmp` source has a NULL domain, so a new provider displaces\nevery other MMP provider — connecting Adjust deactivates a live\nAppsFlyer source, whose postback URL then 404s. Whatever this call\ndeactivated comes back in `source.deactivated_sources`, and a client\nthat ignores it leaves the advertiser to discover the outage from their\nown failing postbacks.\n",
        "operationId": "createConversionSource",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "domain": {
                    "description": "NOT always a domain. For ga4 it is the property resource name, for google_ads the customer id, for meta `act_<id>`.",
                    "nullable": true,
                    "type": "string"
                  },
                  "mmp_provider": {
                    "description": "Required when source_type is `mmp`.",
                    "enum": [
                      "appsflyer",
                      "adjust",
                      "kochava",
                      "branch",
                      "singular"
                    ],
                    "type": "string"
                  },
                  "poi_ids": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  },
                  "source_type": {
                    "enum": [
                      "shopify",
                      "pos_webhook",
                      "pixel",
                      "server_postback",
                      "mmp",
                      "search_console",
                      "store_visits",
                      "qr",
                      "ga4",
                      "meta",
                      "google_ads"
                    ],
                    "type": "string"
                  }
                },
                "required": [
                  "source_type"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "source": {
                      "$ref": "#/components/schemas/CreatedConversionSource"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Created. Carries the full webhook_secret for source types that sign their ingest, and whatever this create deactivated."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Connect a measurement source",
        "tags": [
          "Measurement Sources"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/conversion-source/{sourceId}": {
      "delete": {
        "operationId": "deleteConversionSource",
        "parameters": [
          {
            "in": "path",
            "name": "sourceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Soft-deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Disconnect a measurement source (soft delete)",
        "tags": [
          "Measurement Sources"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      },
      "get": {
        "operationId": "getConversionSource",
        "parameters": [
          {
            "in": "path",
            "name": "sourceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "source": {
                      "$ref": "#/components/schemas/ConversionSource"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The source, redacted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Not the owner."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Fetch one measurement source",
        "tags": [
          "Measurement Sources"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      },
      "patch": {
        "description": "A PATCH never rotates the stored webhook_secret — use the roll endpoint.",
        "operationId": "updateConversionSource",
        "parameters": [
          {
            "in": "path",
            "name": "sourceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "active": {
                    "type": "boolean"
                  },
                  "domain": {
                    "nullable": true,
                    "type": "string"
                  },
                  "poi_ids": {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "source": {
                      "$ref": "#/components/schemas/ConversionSource"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The updated source, redacted."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a measurement source",
        "tags": [
          "Measurement Sources"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/conversion-source/{sourceId}/capability-url": {
      "get": {
        "description": "Returns a pasteable ingest URL for MMP sources whose provider cannot\nsend `X-Trill-Signature`. AppsFlyer's Push API accepts the\n`Authorization` header only and requires extra parameters to travel in\nthe URL, so an AppsFlyer advertiser has no other authenticated ingest.\n\n**The URL IS the credential.** Anyone holding it can send conversions\nfor this source — a capability authenticates the CALLER, not the\nPAYLOAD. Prefer the signed postback wherever the provider can send a\nheader.\n\nIts own endpoint rather than a field on the source read, because that\nread is redacted and cached; a live credential must not enter that\ncache. Minted on demand from the source's current secret, so rolling\nthe secret changes what this returns and invalidates the old URL.\n\nIssued for `mmp` sources only.\n",
        "operationId": "getConversionSourceCapabilityUrl",
        "parameters": [
          {
            "in": "path",
            "name": "sourceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "capability_url": {
                      "format": "uri",
                      "type": "string"
                    },
                    "notice": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The capability URL. Treat as a secret."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "This source cannot carry a capability URL (not an MMP source, or it does not sign its ingest)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get the capability ingest URL for a provider that cannot send a header",
        "tags": [
          "Measurement Sources"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/conversion-source/{sourceId}/deliveries": {
      "get": {
        "description": "The last N inbound deliveries for a source, so \"my provider is not\nsending\" and \"you are dropping what I send\" can be told apart.\n\nBodies are redacted of device identifiers at WRITE time, not on read —\nredacting on read would mean the unredacted value had been stored. A\nredaction marker means the field was PRESENT; an absent value is stored\nabsent, because \"no device id sent\" and \"we did not keep it\" are\ndifferent diagnoses.\n\nA delivery that failed authentication has no source and is not listed\nhere.\n",
        "operationId": "listConversionSourceDeliveries",
        "parameters": [
          {
            "in": "path",
            "name": "sourceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 50,
              "maximum": 200,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "days",
            "required": false,
            "schema": {
              "default": 30,
              "maximum": 30,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "deliveries": {
                      "items": {
                        "$ref": "#/components/schemas/InboundDelivery"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Deliveries, newest first."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "What this source's provider pushed, and what we answered",
        "tags": [
          "Measurement Sources"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/conversion-source/{sourceId}/roll-secret": {
      "post": {
        "description": "Mints a new `webhook_secret` and keeps the OLD one valid for a bounded\noverlap — 24 hours by default, 1–72 configurable — so deliveries already\nin flight, and retries of events signed before the new value was pasted\nin, still verify.\n\nThe response is the only place the new secret appears.\n\nREFUSED, with a stated reason, where the secret is not ours to rotate.\nShopify verifies against the Trillboards app secret as well as the\nsource's own, so rolling would revoke nothing; a Square-backed POS\nwebhook signs with a key Square issued, and rolling it would break\nverification permanently. Only `mmp` and `server_postback` may roll.\n\nRolling also invalidates any capability URL issued for the source.\n",
        "operationId": "rollConversionSourceSecret",
        "parameters": [
          {
            "in": "path",
            "name": "sourceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "overlap_hours": {
                    "default": 24,
                    "maximum": 72,
                    "minimum": 1,
                    "type": "integer"
                  }
                },
                "type": "object"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "source": {
                      "$ref": "#/components/schemas/ConversionSource"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "webhook_secret": {
                      "description": "Shown once. Store it now.",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Rolled. Carries the new secret in full, once."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "This source type's secret is not ours to rotate; the message says which and why."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll a source's signing secret",
        "tags": [
          "Measurement Sources"
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/conversion-sources": {
      "get": {
        "description": "Redacted, and served from a 60-second cache. Because the cache stores\nthe redacted shape, the redaction is an ALLOWLIST: a column not named\nin it never reaches a client or the cache.\n",
        "operationId": "listConversionSources",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "sources": {
                      "items": {
                        "$ref": "#/components/schemas/ConversionSource"
                      },
                      "type": "array"
                    },
                    "success": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "The advertiser's sources."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List this advertiser's measurement sources",
        "tags": [
          "Measurement Sources"
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/advertiser/proof/.well-known/public-key": {
      "get": {
        "description": "Returns the Ed25519 public key used to sign all v2 proofs.\nThird parties can use this key to independently verify signatures.\n\n**No authentication required.**\n",
        "operationId": "getPublicKey",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "algorithm": "ed25519",
                  "documentation": "https://api.trillboards.com/docs/proof-of-play",
                  "keyId": "proof-v2-2026",
                  "publicKey": "a5a9c43785680c62ebcd01ec49a0d6055fd140023df0f142c3e37c6abafce34f",
                  "success": true,
                  "version": "v2"
                },
                "schema": {
                  "$ref": "#/components/schemas/PublicKeyResponse"
                }
              }
            },
            "description": "Public key information"
          }
        },
        "security": [],
        "summary": "Get public key for signature verification",
        "tags": [
          "Public Key"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS https://api.trillboards.com/v1/advertiser/proof/.well-known/public-key\n"
          },
          {
            "label": "Node.js",
            "lang": "javascript",
            "source": "// Cache this — the key rotates rarely — but cache it with a TTL,\n// NOT until a key change announces itself. `keyId` identifies the\n// key HERE, and no proof carries one, so a rotation is invisible in\n// the proof body: after one, a correct proof simply fails to verify.\n//\n// So the refresh trigger is a verification FAILURE, not a keyId\n// mismatch. On a failure, re-fetch this endpoint once and retry the\n// verification before concluding the proof is bad.\nconst res = await fetch(\n  'https://api.trillboards.com/v1/advertiser/proof/.well-known/public-key',\n);\nconst { publicKey, keyId } = await res.json();\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "proof"
      }
    },
    "/v1/advertiser/proof/campaigns/{campaignId}/audience-proof": {
      "get": {
        "description": "Returns per-screen impression counts for the campaign and one\naggregate signature over the roll-up.\n",
        "operationId": "getAudienceProofs",
        "parameters": [
          {
            "in": "path",
            "name": "campaignId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "startDate",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "endDate",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "aggregateSignature": "ed25519=4bd6414e5b3b36a03a4a8c8093350ca1aabb0c8a4f2634f66c943f4521cc8d3d6d23b1a518a2ed8a324395278c6facadf6047bfd93eee597e35fe9235716c6d5",
                    "campaignId": "65f3a1b2c4d5e6f7a8b9c0d1",
                    "generatedAt": "2026-08-31T09:06:20.004Z",
                    "privacyMode": "aggregated",
                    "queryPeriod": {
                      "endDate": "now",
                      "startDate": "2026-08-01T00:00:00Z"
                    },
                    "screenBreakdown": [
                      {
                        "impressions": 28104,
                        "period": {
                          "first": "2026-08-01T06:02:11.000Z",
                          "last": "2026-08-30T23:58:40.000Z"
                        },
                        "screenId": "507f1f77bcf86cd799439022"
                      },
                      {
                        "impressions": 13788,
                        "period": {
                          "first": "2026-08-03T07:15:02.000Z",
                          "last": "2026-08-30T22:41:19.000Z"
                        },
                        "screenId": "507f1f77bcf86cd799439055"
                      }
                    ],
                    "signatureVersion": "v2",
                    "totalImpressions": 41892
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/AudienceProofsResponse"
                }
              }
            },
            "description": "Per-screen impression counts for the campaign, plus one aggregate\nsignature over the whole roll-up. There is no per-impression proof\nhere and no demographic breakdown — `privacyMode` is always\n`aggregated`.\n\nThe aggregate signature covers\n`aggregate.<campaignId>.<totalImpressions>.<generatedAt>`, so it is\nreproducible only against the `totalImpressions` and `generatedAt`\nin the same response body.\n"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized__proof"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound__proof"
          },
          "422": {
            "$ref": "#/components/responses/ProofWindowOutOfRetention"
          },
          "503": {
            "$ref": "#/components/responses/ProofStoreUnavailable"
          }
        },
        "security": [
          {
            "bearerAuth__proof": []
          }
        ],
        "summary": "Get audience-enhanced proofs",
        "tags": [
          "Proofs"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -G \\\n  https://api.trillboards.com/v1/advertiser/proof/campaigns/65f3a1b2c4d5e6f7a8b9c0d1/audience-proof \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -d startDate=2026-08-01T00:00:00Z\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "proof"
      }
    },
    "/v1/advertiser/proof/campaigns/{campaignId}/export-proof": {
      "post": {
        "description": "Export proofs as JSON or CSV for offline verification.",
        "operationId": "exportProofs",
        "parameters": [
          {
            "in": "path",
            "name": "campaignId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "endDate": {
                    "format": "date-time",
                    "type": "string"
                  },
                  "format": {
                    "default": "json",
                    "enum": [
                      "json",
                      "csv"
                    ],
                    "type": "string"
                  },
                  "startDate": {
                    "format": "date-time",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "campaignId": "65f3a1b2c4d5e6f7a8b9c0d1",
                    "checksum": "sha256=41475c9a9dc59a7bf51eaa262489cc877c3c5076d0fbb57d971571371760a567",
                    "data": [
                      {
                        "adId": "65f3a1b2c4d5e6f7a8b9c0d1",
                        "deviceId": "dev_7f3c2a91b45e",
                        "eventType": "complete",
                        "impressionId": "imp_pop_1",
                        "screenId": "507f1f77bcf86cd799439022",
                        "signature": "ed25519=2f7bd966c306a52de48af2cb7af48782541392286bda4cf929b9f9d366e1fafece07c0aa3bc992c8856b793f6574c2723f418be6cdad031ed2989f44df0984f9",
                        "signatureVersion": "v2",
                        "timestamp": "2026-08-30T14:12:03.118Z",
                        "verifiedAt": "2026-08-31T09:10:02.100Z"
                      },
                      {
                        "adId": "65f3a1b2c4d5e6f7a8b9c0d1",
                        "deviceId": "dev_7f3c2a91b45e",
                        "eventType": "complete",
                        "impressionId": "65011e27-dadf-ad30-89dc-f5c036ffe56f",
                        "screenId": "507f1f77bcf86cd799439022",
                        "signature": "ed25519=f2c920e1f1989d01a87d1e5a44657c3453071a1e62d1c73a300fcc3d83b68efb68732740c84b2c7d77c6ca173d176b59f483a2549d76cd1102fabd469a7cf72a",
                        "signatureVersion": "v2",
                        "timestamp": "2026-08-30T14:11:48.002Z",
                        "verifiedAt": "2026-08-31T09:10:02.101Z"
                      }
                    ],
                    "expiresAt": "2026-09-01T09:10:02.104Z",
                    "exportedAt": "2026-08-31T09:10:02.104Z",
                    "format": "json",
                    "proofCount": 2,
                    "publicKeyUrl": "/v1/advertiser/proof/.well-known/public-key",
                    "signatureVersion": "v2"
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/ExportProofsResponse"
                }
              },
              "text/csv": {
                "example": "impressionId,adId,screenId,timestamp,signature,signatureVersion,eventType\nimp_pop_1,65f3a1b2c4d5e6f7a8b9c0d1,507f1f77bcf86cd799439022,2026-08-30T14:12:03.118Z,ed25519=2f7bd966c306a52de48af2cb7af48782541392286bda4cf929b9f9d366e1fafece07c0aa3bc992c8856b793f6574c2723f418be6cdad031ed2989f44df0984f9,v2,complete\n65011e27-dadf-ad30-89dc-f5c036ffe56f,65f3a1b2c4d5e6f7a8b9c0d1,507f1f77bcf86cd799439022,2026-08-30T14:11:48.002Z,ed25519=f2c920e1f1989d01a87d1e5a44657c3453071a1e62d1c73a300fcc3d83b68efb68732740c84b2c7d77c6ca173d176b59f483a2549d76cd1102fabd469a7cf72a,v2,complete\n",
                "schema": {
                  "description": "Header row followed by one row per proof, in this fixed column\norder. Values are not quoted or escaped.\n",
                  "type": "string"
                }
              }
            },
            "description": "The export. **The response media type is chosen by the request\nbody**, not by `Accept`: `format: \"csv\"` returns `text/csv` with a\n`Content-Disposition` filename and no JSON envelope; anything else\nreturns the JSON envelope below.\n\nAn export is capped at 10,000 proofs and is not paginated — a\ncampaign with more plays than that is truncated silently. Page\n`GET /campaigns/{campaignId}/proof` instead when you need all of\nthem.\n\n`checksum` is a SHA-256 over the serialised `data` array only. It\nauthenticates the batch you received; it is not an Ed25519 proof\nsignature, and `expiresAt` is advisory metadata rather than an\nenforced deadline.\n"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized__proof"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound__proof"
          },
          "422": {
            "$ref": "#/components/responses/ProofWindowOutOfRetention"
          },
          "503": {
            "$ref": "#/components/responses/ProofStoreUnavailable"
          }
        },
        "security": [
          {
            "bearerAuth__proof": []
          }
        ],
        "summary": "Export proofs in batch format",
        "tags": [
          "Export"
        ],
        "x-codeSamples": [
          {
            "label": "cURL — JSON",
            "lang": "curl",
            "source": "curl -sS -X POST \\\n  https://api.trillboards.com/v1/advertiser/proof/campaigns/65f3a1b2c4d5e6f7a8b9c0d1/export-proof \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"format\":\"json\",\"startDate\":\"2026-08-01T00:00:00Z\"}'\n"
          },
          {
            "label": "cURL — CSV to a file",
            "lang": "curl",
            "source": "# `format: \"csv\"` changes the response to text/csv and sets a\n# Content-Disposition filename; there is no JSON envelope.\ncurl -sS -X POST -OJ \\\n  https://api.trillboards.com/v1/advertiser/proof/campaigns/65f3a1b2c4d5e6f7a8b9c0d1/export-proof \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"format\":\"csv\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "proof"
      }
    },
    "/v1/advertiser/proof/campaigns/{campaignId}/proof": {
      "get": {
        "description": "Returns cryptographically signed proofs for all completed impressions\nin the specified campaign. Supports cursor-based pagination for large datasets.\n",
        "operationId": "getCampaignProofs",
        "parameters": [
          {
            "description": "Campaign/Advertisement ID",
            "in": "path",
            "name": "campaignId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter proofs after this date (ISO 8601)",
            "in": "query",
            "name": "startDate",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Filter proofs before this date (ISO 8601)",
            "in": "query",
            "name": "endDate",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Maximum number of proofs to return (default 1000, max 10000)",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 1000,
              "maximum": 10000,
              "type": "integer"
            }
          },
          {
            "description": "Pagination cursor from the previous response. A cursor beginning\n`pg~` was issued by the retired PostgreSQL proof path and is refused\nwith 400 `proof_cursor_retired`; restart from page one.\n",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "campaignId": "65f3a1b2c4d5e6f7a8b9c0d1",
                    "generatedAt": "2026-08-31T09:00:00.415Z",
                    "pagination": {
                      "hasMore": true,
                      "limit": 1000,
                      "nextCursor": "ch~2026-08-30T14:11:48.002Z|65011e27-dadf-ad30-89dc-f5c036ffe56f"
                    },
                    "proofs": [
                      {
                        "adId": "65f3a1b2c4d5e6f7a8b9c0d1",
                        "deviceId": "dev_7f3c2a91b45e",
                        "eventType": "complete",
                        "impressionId": "imp_pop_1",
                        "screenId": "507f1f77bcf86cd799439022",
                        "signature": "ed25519=2f7bd966c306a52de48af2cb7af48782541392286bda4cf929b9f9d366e1fafece07c0aa3bc992c8856b793f6574c2723f418be6cdad031ed2989f44df0984f9",
                        "signatureVersion": "v2",
                        "timestamp": "2026-08-30T14:12:03.118Z",
                        "verifiedAt": "2026-08-31T09:00:00.412Z"
                      },
                      {
                        "adId": "65f3a1b2c4d5e6f7a8b9c0d1",
                        "deviceId": "dev_7f3c2a91b45e",
                        "eventType": "complete",
                        "impressionId": "65011e27-dadf-ad30-89dc-f5c036ffe56f",
                        "screenId": "507f1f77bcf86cd799439022",
                        "signature": "ed25519=f2c920e1f1989d01a87d1e5a44657c3453071a1e62d1c73a300fcc3d83b68efb68732740c84b2c7d77c6ca173d176b59f483a2549d76cd1102fabd469a7cf72a",
                        "signatureVersion": "v2",
                        "timestamp": "2026-08-30T14:11:48.002Z",
                        "verifiedAt": "2026-08-31T09:00:00.413Z"
                      }
                    ],
                    "proofsReturned": 2,
                    "publicKeyUrl": "/v1/advertiser/proof/.well-known/public-key",
                    "queryPeriod": {
                      "endDate": "now",
                      "startDate": "2026-08-01T00:00:00Z"
                    },
                    "signatureVersion": "v2",
                    "totalImpressions": 41892
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/CampaignProofsResponse"
                }
              }
            },
            "description": "One page of signed proofs, newest first.\n\n`data.totalImpressions` is populated **only on the first page** (a\nrequest with no `cursor`); on every later page it is `null`, because\nthe count is not recomputed per page. Read the total once, from\npage one.\n"
          },
          "400": {
            "$ref": "#/components/responses/ProofCursorRetired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized__proof"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound__proof"
          },
          "422": {
            "$ref": "#/components/responses/ProofWindowOutOfRetention"
          },
          "503": {
            "$ref": "#/components/responses/ProofStoreUnavailable"
          }
        },
        "security": [
          {
            "bearerAuth__proof": []
          }
        ],
        "summary": "Get proofs for a campaign",
        "tags": [
          "Proofs"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -G \\\n  https://api.trillboards.com/v1/advertiser/proof/campaigns/65f3a1b2c4d5e6f7a8b9c0d1/proof \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -d startDate=2026-08-01T00:00:00Z \\\n  -d limit=1000\n"
          },
          {
            "label": "Node.js — page the whole campaign",
            "lang": "javascript",
            "source": "// `nextCursor` is opaque and pins the sequence to one storage\n// backend. Pass it back verbatim; never parse or rebuild it.\nasync function* campaignProofs(campaignId, jwt) {\n  let cursor;\n  do {\n    const qs = new URLSearchParams({ limit: '1000' });\n    if (cursor) qs.set('cursor', cursor);\n    const res = await fetch(\n      `https://api.trillboards.com/v1/advertiser/proof/campaigns/${campaignId}/proof?${qs}`,\n      { headers: { Authorization: `Bearer ${jwt}` } },\n    );\n    if (!res.ok) throw new Error(`proof fetch failed: ${res.status}`);\n    const { data } = await res.json();\n    yield* data.proofs;\n    cursor = data.pagination.nextCursor;\n  } while (cursor);\n}\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "proof"
      }
    },
    "/v1/advertiser/proof/screens/{screenId}/proof": {
      "get": {
        "description": "Returns proofs for all completed impressions on the specified screen.",
        "operationId": "getScreenProofs",
        "parameters": [
          {
            "description": "Screen ID",
            "in": "path",
            "name": "screenId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "startDate",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "endDate",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 500,
              "maximum": 5000,
              "type": "integer"
            }
          },
          {
            "description": "Pagination cursor from the previous response. A cursor beginning\n`pg~` was issued by the retired PostgreSQL proof path and is refused\nwith 400 `proof_cursor_retired`; restart from page one.\n",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "generatedAt": "2026-08-31T09:04:11.871Z",
                    "location": {
                      "address": "2250 NW 2nd Ave, Miami, FL 33127",
                      "latitude": 25.800179,
                      "longitude": -80.199463
                    },
                    "pagination": {
                      "hasMore": false,
                      "limit": 500,
                      "nextCursor": null
                    },
                    "proofs": [
                      {
                        "adId": "65f3a1b2c4d5e6f7a8b9c0d1",
                        "deviceId": "dev_7f3c2a91b45e",
                        "eventType": "complete",
                        "impressionId": "imp_pop_1",
                        "screenId": "507f1f77bcf86cd799439022",
                        "signature": "ed25519=2f7bd966c306a52de48af2cb7af48782541392286bda4cf929b9f9d366e1fafece07c0aa3bc992c8856b793f6574c2723f418be6cdad031ed2989f44df0984f9",
                        "signatureVersion": "v2",
                        "timestamp": "2026-08-30T14:12:03.118Z",
                        "verifiedAt": "2026-08-31T09:04:11.870Z"
                      }
                    ],
                    "screenId": "507f1f77bcf86cd799439022",
                    "screenName": "Wynwood Marketplace — North Entrance",
                    "signatureVersion": "v2",
                    "totalProofs": 1
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/ScreenProofsResponse"
                }
              }
            },
            "description": "One page of signed proofs for this screen, newest first.\n\n`data.totalProofs` counts the proofs **in this page**, not the\nscreen's lifetime total — there is no campaign-style total here.\nPage with `pagination.nextCursor` until it is `null`.\n"
          },
          "400": {
            "$ref": "#/components/responses/ProofCursorRetired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized__proof"
          },
          "422": {
            "$ref": "#/components/responses/ProofWindowOutOfRetention"
          },
          "503": {
            "$ref": "#/components/responses/ProofStoreUnavailable"
          }
        },
        "security": [
          {
            "bearerAuth__proof": []
          }
        ],
        "summary": "Get proofs for a screen",
        "tags": [
          "Proofs"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS -G \\\n  https://api.trillboards.com/v1/advertiser/proof/screens/507f1f77bcf86cd799439022/proof \\\n  -H \"Authorization: Bearer $TRILLBOARDS_JWT\" \\\n  -d limit=500\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "proof"
      }
    },
    "/v1/advertiser/proof/signature-info": {
      "get": {
        "description": "Returns information about the signature format for integrators.",
        "operationId": "getSignatureInfo",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "signatureInfo": {
                    "algorithm": "Ed25519",
                    "currentVersion": "v2",
                    "documentation": "https://api.trillboards.com/docs/proof-of-play",
                    "legacySupport": {
                      "v1": {
                        "algorithm": "HMAC-SHA256",
                        "note": "Deprecated. Use v2 Ed25519 signatures for third-party verification.",
                        "signaturePrefix": "sha256="
                      }
                    },
                    "payloadFormat": "version.timestamp.adId.impressionId.screenId.deviceId",
                    "publicKey": "a5a9c43785680c62ebcd01ec49a0d6055fd140023df0f142c3e37c6abafce34f",
                    "publicKeyUrl": "/v1/advertiser/proof/.well-known/public-key",
                    "signaturePrefix": "ed25519=",
                    "verificationEndpoint": "/v1/advertiser/proof/verify-proof"
                  },
                  "success": true
                },
                "schema": {
                  "$ref": "#/components/schemas/SignatureInfoResponse"
                }
              }
            },
            "description": "Everything needed to verify a proof without reading this page: the\nactive key, the exact payload construction, and the prefix that\nidentifies each signature version. `publicKey` is the same value\nreturned by `/.well-known/public-key`.\n"
          }
        },
        "security": [],
        "summary": "Get signature format documentation",
        "tags": [
          "Public Key"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS https://api.trillboards.com/v1/advertiser/proof/signature-info\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "proof"
      }
    },
    "/v1/advertiser/proof/verify-proof": {
      "post": {
        "description": "Verify the cryptographic signature of a proof.\nSupports both v1 (HMAC) and v2 (Ed25519) signatures.\n\n**No authentication required** - third parties can verify proofs independently.\n",
        "operationId": "verifyProof",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "adId": "campaign-123",
                "deviceId": "device-abc",
                "impressionId": "imp-456",
                "screenId": "screen-789",
                "signature": "ed25519=a1b2c3...",
                "timestamp": 1705401600000
              },
              "schema": {
                "$ref": "#/components/schemas/VerifyProofRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "mismatch": {
                    "summary": "Signature does not match the payload",
                    "value": {
                      "publicKeyUrl": "/v1/advertiser/proof/.well-known/public-key",
                      "success": true,
                      "verification": {
                        "reason": "signature_mismatch",
                        "valid": false,
                        "version": "v2"
                      },
                      "verifiedAt": "2026-08-31T09:08:44.201Z"
                    }
                  },
                  "valid": {
                    "summary": "Signature verifies",
                    "value": {
                      "publicKeyUrl": "/v1/advertiser/proof/.well-known/public-key",
                      "success": true,
                      "verification": {
                        "valid": true,
                        "version": "v2"
                      },
                      "verifiedAt": "2026-08-31T09:08:44.201Z"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/VerifyProofResponse"
                }
              }
            },
            "description": "The verification ran. **`200` does not mean the proof is valid** —\nread `verification.valid`. A signature that fails to verify is a\nsuccessful verification of a bad proof, so it is still a `200`.\n\nWhen `valid` is `false`, `verification.reason` says why:\n`missing_required_fields`, `signature_mismatch`,\n`signature_length_mismatch`, `public_key_not_configured`, or\n`verification_error`.\n"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "Missing required fields"
          }
        },
        "security": [],
        "summary": "Verify a proof signature",
        "tags": [
          "Verification"
        ],
        "x-codeSamples": [
          {
            "label": "cURL",
            "lang": "curl",
            "source": "curl -sS https://api.trillboards.com/v1/advertiser/proof/verify-proof \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n        \"signature\": \"ed25519=2f7bd966c306a52de48af2cb7af48782541392286bda4cf929b9f9d366e1fafece07c0aa3bc992c8856b793f6574c2723f418be6cdad031ed2989f44df0984f9\",\n        \"timestamp\": 1788134400000,\n        \"adId\": \"65f3a1b2c4d5e6f7a8b9c0d1\",\n        \"impressionId\": \"imp_pop_1\",\n        \"screenId\": \"507f1f77bcf86cd799439022\",\n        \"deviceId\": \"dev_7f3c2a91b45e\"\n      }'\n"
          },
          {
            "label": "Python — verify offline, without calling us",
            "lang": "python",
            "source": "# A v2 proof is verifiable with the published key alone. Preferring\n# this to the endpoint means an auditor never has to trust our\n# uptime or our answer.\nfrom datetime import datetime, timezone\n\nimport requests\nfrom cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey\nfrom cryptography.exceptions import InvalidSignature\n\nBASE = \"https://api.trillboards.com/v1/advertiser/proof\"\npub = requests.get(f\"{BASE}/.well-known/public-key\").json()[\"publicKey\"]\nkey = Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub))\n\ndef signed_timestamp(ts):\n    # THE SIGNATURE COVERS EPOCH MILLISECONDS, and the API hands you\n    # an ISO-8601 string. Signing the string you were given rebuilds\n    # different bytes and reports every genuine proof as invalid.\n    if isinstance(ts, (int, float)):\n        return str(int(ts))\n    dt = datetime.fromisoformat(ts.replace(\"Z\", \"+00:00\"))\n    if dt.tzinfo is None:\n        dt = dt.replace(tzinfo=timezone.utc)\n    return str(round(dt.timestamp() * 1000))\n\ndef verify(proof):\n    # Field order is fixed and the separator is a literal dot.\n    payload = \".\".join([\n        proof[\"signatureVersion\"],\n        signed_timestamp(proof[\"timestamp\"]),\n        proof[\"adId\"],\n        proof[\"impressionId\"],\n        proof[\"screenId\"],\n        proof[\"deviceId\"],\n    ]).encode()\n    sig = bytes.fromhex(proof[\"signature\"].removeprefix(\"ed25519=\"))\n    try:\n        key.verify(sig, payload)\n        return True\n    except InvalidSignature:\n        # Before reporting a bad proof, re-fetch the public key once\n        # and retry: a signing-key rotation is indistinguishable\n        # from a bad signature here (see the key-caching note on\n        # /.well-known/public-key).\n        return False\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "proof"
      }
    },
    "/v1/attribution/event/mobile": {
      "post": {
        "description": "Receives mobile ad impression/click postbacks from DSP partners.\nThese events are ingested as touchpoints in the cross-channel\nattribution pipeline, enabling DOOH -> mobile -> conversion chains.\n\nNo authentication required — DSP postback URLs are typically\npre-configured and fire-and-forget.\n",
        "operationId": "ingestMobileEvent",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "advertiserId": "adv_456",
                "campaignId": "camp_abc123",
                "creativeId": "cre_xyz789",
                "dspPartner": "thetradedesk",
                "eventType": "click",
                "maidHash": "a1b2c3d4e5f6...",
                "placementId": "placement_001"
              },
              "schema": {
                "$ref": "#/components/schemas/MobileEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "touchpointId": "550e8400-e29b-41d4-a716-446655440000"
                },
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "touchpointId": {
                      "format": "uuid",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Event ingested"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [],
        "summary": "DSP mobile postback",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/event/mobile\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"maidHash\":\"a1b2c3d4e5f6...\",\"campaignId\":\"camp_abc123\",\"creativeId\":\"cre_xyz789\",\"advertiserId\":\"adv_456\",\"dspPartner\":\"thetradedesk\",\"eventType\":\"click\",\"placementId\":\"placement_001\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/event/transaction/{sourceId}": {
      "post": {
        "description": "Ingests transaction/purchase data for conversion attribution from a\nPoint-of-sale or Server-postback conversion source. Sources include POS\nreceipts and backend purchase events.\n\nAuthentication is a per-source HMAC: sign the raw request body with the\nsource's `webhook_secret` and send it as\n`X-Trill-Signature: sha256=<hex>` (timing-safe, fail-closed). The\nadvertiser is taken from the source, never the body. A stable `eventId`\n(order id) makes retries exactly-once via the dedup ledger.\n\nTransaction data is matched to exposed devices via MAID hash\nto prove: \"This person saw the ad AND made a purchase.\"\n\n(The legacy unauthenticated bare `/v1/attribution/event/transaction`\nis retired and returns 410.)\n",
        "operationId": "ingestTransactionEvent",
        "parameters": [
          {
            "description": "The conversion source id (pos_webhook or server_postback).",
            "in": "path",
            "name": "sourceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "sha256=<hex HMAC of the raw body keyed by the source webhook_secret>",
            "in": "header",
            "name": "X-Trill-Signature",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "basketItems": [
                  {
                    "name": "Premium Widget",
                    "price_cents": 4999,
                    "quantity": 1,
                    "sku": "SKU-001"
                  }
                ],
                "campaignId": "camp_abc123",
                "eventId": "order_10042",
                "poiId": "550e8400-e29b-41d4-a716-446655440000",
                "transactionAmountCents": 4999
              },
              "schema": {
                "$ref": "#/components/schemas/TransactionEventRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "examples": {
                  "firstDelivery": {
                    "summary": "First delivery — a touchpoint is created",
                    "value": {
                      "deduped": false,
                      "success": true,
                      "touchpointId": "550e8400-e29b-41d4-a716-446655440000"
                    }
                  },
                  "replay": {
                    "summary": "Replay of the same eventId — nothing written",
                    "value": {
                      "deduped": true,
                      "success": true,
                      "touchpointId": null
                    }
                  }
                },
                "schema": {
                  "properties": {
                    "deduped": {
                      "description": "`touchpointId === null`. `true` means this delivery was a\nreplay and nothing new was written.\n",
                      "type": "boolean"
                    },
                    "success": {
                      "description": "Always `true` on this path.",
                      "type": "boolean"
                    },
                    "touchpointId": {
                      "description": "The created touchpoint, or `null` when the dedup ledger\nalready held this `(sourceId, eventId)` pair. Only sent as\na UUID on the first delivery.\n",
                      "format": "uuid",
                      "nullable": true,
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Transaction event ingested. A REPLAY of an already-seen `eventId`\nalso returns 201 — with `touchpointId: null` and `deduped: true`.\nTreat 201 as \"the ledger holds this event\", not as \"a row was\nwritten by this call\".\n"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [],
        "summary": "Transaction data ingestion (source-scoped, signed)",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/event/transaction/{sourceId}\" \\\n  -H \"X-Trill-Signature: <X-Trill-Signature>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"campaignId\":\"camp_abc123\",\"transactionAmountCents\":4999,\"eventId\":\"order_10042\",\"basketItems\":[{\"sku\":\"SKU-001\",\"name\":\"Premium Widget\",\"quantity\":1,\"price_cents\":4999}],\"poiId\":\"550e8400-e29b-41d4-a716-446655440000\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/event/web": {
      "post": {
        "description": "Ingests a web conversion observed by the conversion pixel. Unauthenticated;\nrate limited to 60 requests/minute per client.\n\n> **Success is not an acknowledgement that the event was stored.** A valid\n> request returns `204` immediately and ingestion continues asynchronously;\n> an internal ingest failure after validation still returns `204`. Do not\n> treat `204` as proof of durability, and do not retry on it.\n",
        "operationId": "ingestWebConversionEvent",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "advertiser_id": {
                    "type": "string"
                  },
                  "campaign_id": {
                    "type": "string"
                  },
                  "event_type": {
                    "description": "The conversion type being reported",
                    "type": "string"
                  },
                  "visitor_id": {
                    "description": "Pixel-assigned visitor identifier, when available",
                    "type": "string"
                  }
                },
                "required": [
                  "advertiser_id",
                  "event_type"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "Accepted. Empty body."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "error": {
                      "example": "advertiser_id and event_type required",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "`advertiser_id` or `event_type` missing."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "error": {
                      "example": "Too many requests",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Rate limit exceeded (60/minute)."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "error": {
                      "example": "Internal error",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Internal error."
          }
        },
        "security": [],
        "summary": "Report a web conversion",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/event/web\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"advertiser_id\":\"<advertiser_id>\",\"event_type\":\"<event_type>\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/pixel.js": {
      "get": {
        "description": "Returns the Trillboards web conversion pixel as JavaScript. Embed it on an\nadvertiser site to report web conversions to `/v1/attribution/event/web`.\n\nUnauthenticated, `Access-Control-Allow-Origin: *`, cached for one hour.\nServed as `application/javascript; charset=utf-8` with\n`X-Content-Type-Options: nosniff` — **this endpoint does not return JSON.**\n",
        "operationId": "getAttributionPixelScript",
        "responses": {
          "200": {
            "content": {
              "application/javascript": {
                "example": "/**\n * Trillboards Retargeting Pixel v1.0.0\n * Lightweight attribution pixel for advertiser websites.\n * Respects: DNT, GPC, IAB TCF v2.\n *\n * Usage:\n *   <script src=\"https://api.trillboards.com/v1/attribution/pixel.js\"\n *           data-advertiser-id=\"ADV_ID\"\n *           data-campaign-id=\"CAMP_ID\"></script>\n */\n(function(w, d) {\n  'use strict';\n  var SDK_VERSION = '1.0.0';\n  /* … 4,045 bytes total; served from cache with Cache-Control: public, max-age=3600 … */\n})(window, document);\n",
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "The pixel script",
            "headers": {
              "Access-Control-Allow-Origin": {
                "schema": {
                  "example": "*",
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "example": "public, max-age=3600",
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "content": {
              "text/html": {
                "schema": {
                  "example": "// Error loading pixel",
                  "type": "string"
                }
              }
            },
            "description": "The script could not be read. The body is the JavaScript comment\n`// Error loading pixel` rather than a JSON error object, so that a\n`<script>` tag fails silently instead of throwing a syntax error.\n"
          }
        },
        "security": [],
        "summary": "Web conversion pixel script",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/attribution/pixel.js\"\n"
          }
        ],
        "x-trillboards-agent-safe": true,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/pixel/{advertiserId}/{campaignId}": {
      "get": {
        "description": "Returns a 1x1 transparent GIF for web conversion tracking. Embed as:\n```html\n<img src=\"https://api.trillboards.com/v1/attribution/pixel/{advertiserId}/{campaignId}\" width=\"1\" height=\"1\" />\n```\n\nEach pixel fire creates a touchpoint in the cross-channel attribution\npipeline using a visitor ID (SHA-256 of IP + User-Agent).\nNo cookies, no JavaScript — pure server-side tracking.\n",
        "operationId": "getTrackingPixel",
        "parameters": [
          {
            "in": "path",
            "name": "advertiserId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "campaignId",
            "required": true,
            "schema": {
              "pattern": "^[a-zA-Z0-9_-]{1,128}$",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "image/gif": {
                "example": "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
                "schema": {
                  "format": "binary",
                  "type": "string"
                }
              }
            },
            "description": "1x1 transparent GIF"
          }
        },
        "security": [],
        "summary": "Conversion tracking pixel",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/attribution/pixel/{advertiserId}/{campaignId}\"\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/postback/adjust": {
      "post": {
        "description": "Receives Adjust callbacks and ingests them as cross-channel touchpoints,\njoining mobile app conversions to DOOH exposures (raw device ids are\nhashed at the edge).\n\nAuthentication is an HMAC-SHA256 over the JSON request body, sent as\n`X-Signature: sha256=<hex>` (the `Authorization` header is also\naccepted), keyed by the shared `ADJUST_POSTBACK_SECRET`. Retries are\nde-duplicated via `event_token` + `adid` + `created_at`.\n",
        "operationId": "ingestAdjustPostback",
        "parameters": [
          {
            "description": "sha256=<hex HMAC-SHA256 of the JSON body keyed by ADJUST_POSTBACK_SECRET>. May instead be sent as the Authorization header.",
            "in": "header",
            "name": "X-Signature",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "activity_kind": "event",
                "adid": "18546f6171f67e29d1cb983322ad51d9",
                "campaign_id": "camp_abc123",
                "campaign_name": "winter_promo",
                "created_at": "1709129100",
                "event_token": "abc123",
                "gps_adid": "38400000-8cf0-11bd-b23e-10b96e40000d"
              },
              "schema": {
                "$ref": "#/components/schemas/AdjustPostbackRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/PostbackAck"
                }
              }
            },
            "description": "Postback accepted and ingested"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "error": "not_configured",
                  "message": "adjust postback is not configured. Use the per-source signed endpoint POST /v1/attribution/postback/adjust/:sourceId with your source's signing secret."
                },
                "schema": {
                  "$ref": "#/components/schemas/NotConfiguredError"
                }
              }
            },
            "description": "Provider postback is not configured — the shared signing secret is\nunset. Use the per-source signed endpoint instead.\n"
          }
        },
        "security": [],
        "summary": "Adjust MMP postback",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/postback/adjust\" \\\n  -H \"X-Signature: <X-Signature>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"activity_kind\":\"event\",\"event_token\":\"abc123\",\"adid\":\"18546f6171f67e29d1cb983322ad51d9\",\"created_at\":\"1709129100\",\"gps_adid\":\"38400000-8cf0-11bd-b23e-10b96e40000d\",\"campaign_name\":\"winter_promo\",\"campaign_id\":\"camp_abc123\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/postback/appsflyer": {
      "post": {
        "description": "Receives AppsFlyer install/event postbacks and ingests them as\ncross-channel touchpoints, joining mobile app conversions to DOOH\nexposures for the same device (raw device ids are hashed at the edge).\n\nAuthentication is an HMAC-SHA256 over the JSON request body, sent as\n`X-Signature: sha256=<hex>` (the `Authorization` header is also\naccepted), keyed by the shared `APPSFLYER_POSTBACK_SECRET`. Retries are\nde-duplicated via `event_id` (or `appsflyer_id` + `event_time`).\n",
        "operationId": "ingestAppsflyerPostback",
        "parameters": [
          {
            "description": "sha256=<hex HMAC-SHA256 of the JSON body keyed by APPSFLYER_POSTBACK_SECRET>. May instead be sent as the Authorization header.",
            "in": "header",
            "name": "X-Signature",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "advertising_id": "38400000-8cf0-11bd-b23e-10b96e40000d",
                "appsflyer_id": "1707912000-1234567",
                "campaign_id": "camp_abc123",
                "event_id": "af_evt_10042",
                "event_name": "af_purchase",
                "event_time": "2026-02-28 14:05:00"
              },
              "schema": {
                "$ref": "#/components/schemas/AppsflyerPostbackRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/PostbackAck"
                }
              }
            },
            "description": "Postback accepted and ingested"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "error": "not_configured",
                  "message": "appsflyer postback is not configured. Use the per-source signed endpoint POST /v1/attribution/postback/appsflyer/:sourceId with your source's signing secret."
                },
                "schema": {
                  "$ref": "#/components/schemas/NotConfiguredError"
                }
              }
            },
            "description": "Provider postback is not configured — the shared signing secret is\nunset. Use the per-source signed endpoint instead.\n"
          }
        },
        "security": [],
        "summary": "AppsFlyer MMP postback",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/postback/appsflyer\" \\\n  -H \"X-Signature: <X-Signature>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_name\":\"af_purchase\",\"event_id\":\"af_evt_10042\",\"appsflyer_id\":\"1707912000-1234567\",\"event_time\":\"2026-02-28 14:05:00\",\"advertising_id\":\"38400000-8cf0-11bd-b23e-10b96e40000d\",\"campaign_id\":\"camp_abc123\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/postback/kochava": {
      "post": {
        "description": "Receives Kochava postbacks and ingests them as cross-channel\ntouchpoints, joining mobile app conversions to DOOH exposures (raw\ndevice ids are hashed at the edge).\n\nAuthentication is an HMAC-SHA256 over the JSON request body, sent as\n`X-Signature: sha256=<hex>` (the `Authorization` header is also\naccepted), keyed by the shared `KOCHAVA_POSTBACK_SECRET`. Retries are\nde-duplicated via `event_id`.\n",
        "operationId": "ingestKochavaPostback",
        "parameters": [
          {
            "description": "sha256=<hex HMAC-SHA256 of the JSON body keyed by KOCHAVA_POSTBACK_SECRET>. May instead be sent as the Authorization header.",
            "in": "header",
            "name": "X-Signature",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "campaign_id": "camp_abc123",
                "device_id": "38400000-8cf0-11bd-b23e-10b96e40000d",
                "event_id": "kchv_evt_88213",
                "event_name": "purchase",
                "event_time": "1709129100"
              },
              "schema": {
                "$ref": "#/components/schemas/KochavaPostbackRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/PostbackAck"
                }
              }
            },
            "description": "Postback accepted and ingested"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "error": "not_configured",
                  "message": "kochava postback is not configured. Use the per-source signed endpoint POST /v1/attribution/postback/kochava/:sourceId with your source's signing secret."
                },
                "schema": {
                  "$ref": "#/components/schemas/NotConfiguredError"
                }
              }
            },
            "description": "Provider postback is not configured — the shared signing secret is\nunset. Use the per-source signed endpoint instead.\n"
          }
        },
        "security": [],
        "summary": "Kochava MMP postback",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/postback/kochava\" \\\n  -H \"X-Signature: <X-Signature>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_name\":\"purchase\",\"event_id\":\"kchv_evt_88213\",\"device_id\":\"38400000-8cf0-11bd-b23e-10b96e40000d\",\"campaign_id\":\"camp_abc123\",\"event_time\":\"1709129100\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/postback/{provider}/c/{token}": {
      "get": {
        "description": "The same ingest, the same capability, the same handler — with the event\nfields in the QUERY STRING instead of a JSON body.\n\nOffered because two of the five supported providers default to this\ntransport (measured from their published documentation 2026-09-19):\n\n- **Adjust** server callbacks default to GET. POST is a per-callback\n  setting the advertiser has to find and change.\n- **Kochava** documents its custom postback's data as key/value pairs\n  in the destination URL's query string and publishes no method\n  selector at all.\n\nWhile the postback routes were POST-only, an advertiser on either\nprovider's default configured a callback that 404'd — and a 404 on an\nunregistered method never reaches the inbound delivery log, so nothing\nrecorded it. The portal showed \"not sending yet\" for a postback the\nprovider believed it was delivering.\n\n**GET is offered ONLY on the capability path.** The signed route\nauthenticates an HMAC over the request body, and a GET has none, so\naccepting GET there would mean accepting an unsigned request on the one\nroute whose purpose is proving the payload. A capability authenticates\nthe caller, which is all any GET can do.\n\nRaw device identifiers sent this way (`idfa`, `gps_adid`,\n`advertising_id`, …) are stripped from the URL before anything logs it,\nand redacted out of the stored delivery body exactly as on the POST\npath.\n",
        "operationId": "ingestCapabilityScopedMmpPostbackViaGet",
        "parameters": [
          {
            "description": "MMP provider; must match the source's configured mmp_provider.",
            "in": "path",
            "name": "provider",
            "required": true,
            "schema": {
              "enum": [
                "appsflyer",
                "adjust",
                "kochava",
                "branch",
                "singular"
              ],
              "type": "string"
            }
          },
          {
            "description": "The capability minted for this source. It IS the credential — anyone holding it can send conversions for the source.",
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Any field of MmpPostbackRequest may be sent as a query parameter; the recognized union is identical to the POST body's.",
            "in": "query",
            "name": "event_name",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "deduped": false,
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/PostbackDedupAck"
                }
              }
            },
            "description": "Postback accepted; `deduped` is true when a retry was collapsed"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [],
        "summary": "Per-source MMP postback over GET, for providers that default to it",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -G \\\n  \"https://api.trillboards.com/v1/attribution/postback/{provider}/c/{token}\" \\\n  --data-urlencode \"event_name=af_purchase\" \\\n  --data-urlencode \"event_id=af_evt_10042\" \\\n  --data-urlencode \"advertising_id=38400000-8cf0-11bd-b23e-10b96e40000d\" \\\n  --data-urlencode \"campaign_id=camp_abc123\"\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      },
      "post": {
        "description": "The same ingest as the signed per-source postback, for providers whose\nconfigurator cannot send a custom header.\n\nAppsFlyer's Push API accepts the `Authorization` header only, states\nthat custom headers are unsupported, and requires any extra parameter\nto travel in the URL — so an advertiser on AppsFlyer cannot satisfy the\n`X-Trill-Signature` contract at all. This route carries the whole\ncredential in the path instead.\n\n**This is weaker than the signed route and is offered only where that\none is impossible.** A capability authenticates the CALLER, not the\nPAYLOAD: anyone holding the URL can post conversions for that source,\nwhereas an HMAC over the body proves both. Prefer\n`/v1/attribution/postback/{provider}/{sourceId}` wherever the provider\ncan send a header.\n\nThe token is minted per source at\n`GET /v1/advertiser/conversion-source/{sourceId}/capability-url` and is\nsigned with that source's own `webhook_secret`, so it authorizes only\nthat source and **rolling the secret revokes it**. It does not expire.\nTreat the URL as a secret.\n",
        "operationId": "ingestCapabilityScopedMmpPostback",
        "parameters": [
          {
            "description": "MMP provider; must match the source's configured mmp_provider.",
            "in": "path",
            "name": "provider",
            "required": true,
            "schema": {
              "enum": [
                "appsflyer",
                "adjust",
                "kochava",
                "branch",
                "singular"
              ],
              "type": "string"
            }
          },
          {
            "description": "The capability minted for this source. It IS the credential — anyone holding it can send conversions for the source.",
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "advertising_id": "38400000-8cf0-11bd-b23e-10b96e40000d",
                "campaign_id": "camp_abc123",
                "event_id": "af_evt_10042",
                "event_name": "af_purchase"
              },
              "schema": {
                "$ref": "#/components/schemas/MmpPostbackRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "deduped": false,
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/PostbackDedupAck"
                }
              }
            },
            "description": "Postback accepted; `deduped` is true when a retry was collapsed"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [],
        "summary": "Per-source MMP postback authenticated by a capability URL",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/postback/{provider}/c/{token}\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_name\":\"af_purchase\",\"event_id\":\"af_evt_10042\",\"advertising_id\":\"38400000-8cf0-11bd-b23e-10b96e40000d\",\"campaign_id\":\"camp_abc123\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/postback/{provider}/{sourceId}": {
      "post": {
        "description": "Authenticated per-source MMP postback. Unlike the shared-secret routes,\nthis consumes the per-source `webhook_secret` the advertiser configures\nin their MMP dashboard, so the correct source flips to `receiving` and\nevents carry that source's advertiser. The provider must match the\nsource's configured `mmp_provider`.\n\nAuthentication is an HMAC-SHA256 over the RAW request body, sent as\n`X-Trill-Signature: sha256=<hex>` (timing-safe, fail-closed).\nProvider-native stable ids make retries exactly-once via the dedup\nledger; the response `deduped` flag reports when a retry was collapsed.\n",
        "operationId": "ingestSourceScopedMmpPostback",
        "parameters": [
          {
            "description": "MMP provider; must match the source's configured mmp_provider.",
            "in": "path",
            "name": "provider",
            "required": true,
            "schema": {
              "enum": [
                "appsflyer",
                "adjust",
                "kochava",
                "branch",
                "singular"
              ],
              "type": "string"
            }
          },
          {
            "description": "The MMP conversion source id.",
            "in": "path",
            "name": "sourceId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "sha256=<hex HMAC-SHA256 of the RAW request body keyed by the source webhook_secret>",
            "in": "header",
            "name": "X-Trill-Signature",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "advertising_id": "38400000-8cf0-11bd-b23e-10b96e40000d",
                "campaign_id": "camp_abc123",
                "event_id": "af_evt_10042",
                "event_name": "af_purchase"
              },
              "schema": {
                "$ref": "#/components/schemas/MmpPostbackRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "deduped": false,
                  "status": "ok"
                },
                "schema": {
                  "$ref": "#/components/schemas/PostbackDedupAck"
                }
              }
            },
            "description": "Postback accepted; `deduped` is true when a retry was collapsed"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [],
        "summary": "Per-source signed MMP postback",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/postback/{provider}/{sourceId}\" \\\n  -H \"X-Trill-Signature: <X-Trill-Signature>\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"event_name\":\"af_purchase\",\"event_id\":\"af_evt_10042\",\"advertising_id\":\"38400000-8cf0-11bd-b23e-10b96e40000d\",\"campaign_id\":\"camp_abc123\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": true,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/provider-impression": {
      "get": {
        "description": "Impression callback for an external ad provider, callable as a tracking-pixel\nURL. Identical semantics to the `POST` form; parameters are read from the\nquery string instead of the body.\n\nAuthentication is the HMAC-signed `t` parameter itself — there is no API key.\nVerification is constant-time and fails closed.\n\n> **This intake is disabled by default.** When the intake flag is off the\n> endpoint returns `404` without touching the database, so a `404` here means\n> \"not enabled\", not \"wrong URL\".\n\nFailures are deliberately never surfaced to the calling ad server: an\ninternal error returns the same empty `204` as success.\n",
        "operationId": "ingestProviderImpressionViaGet",
        "parameters": [
          {
            "description": "HMAC-signed line reference. This is the credential.",
            "in": "query",
            "name": "t",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Provider-side impression id.",
            "in": "query",
            "name": "imp",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/ProviderImpressionAccepted"
          },
          "403": {
            "$ref": "#/components/responses/ProviderImpressionInvalidRef"
          },
          "404": {
            "$ref": "#/components/responses/ProviderImpressionNotFound"
          }
        },
        "security": [],
        "summary": "Provider impression callback (GET)",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS \"https://api.trillboards.com/v1/attribution/provider-impression\"\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      },
      "post": {
        "description": "Impression callback for an external ad provider. See the `GET` form for the\nshared semantics — the intake flag, the HMAC `t` credential, and the\ndeliberate `204`-on-error behaviour all apply identically.\n",
        "operationId": "ingestProviderImpression",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "imp": {
                    "description": "Provider-side impression id.",
                    "type": "string"
                  },
                  "t": {
                    "description": "HMAC-signed line reference. This is the credential.",
                    "type": "string"
                  }
                },
                "required": [
                  "t"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "$ref": "#/components/responses/ProviderImpressionAccepted"
          },
          "403": {
            "$ref": "#/components/responses/ProviderImpressionInvalidRef"
          },
          "404": {
            "$ref": "#/components/responses/ProviderImpressionNotFound"
          }
        },
        "security": [],
        "summary": "Provider impression callback (POST)",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/provider-impression\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"t\":\"<t>\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    },
    "/v1/attribution/qr-signal": {
      "post": {
        "description": "Receives device fingerprint signals for a QR scan, used to link a scan to an\nexposure. Unauthenticated; rate limited to 60 requests/minute per client.\n\nAccepts `application/json` and also `text/plain`, so it can be called with\n`navigator.sendBeacon()` — a plain-text body is parsed as JSON.\n\nReturns `202` as soon as the body validates; the signals are processed\nfire-and-forget, so a `202` does not mean the scan was successfully linked.\n",
        "operationId": "ingestQrBeaconSignal",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QrBeaconSignal"
              }
            },
            "text/plain": {
              "schema": {
                "description": "A JSON document sent as text, for `navigator.sendBeacon()`.",
                "type": "string"
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "content": {
              "application/json": {
                "example": {
                  "ok": true
                },
                "schema": {
                  "properties": {
                    "ok": {
                      "description": "Constant. The ack is written before\n`qrAttributionService.processBeaconSignals()` runs, so it\nreports RECEIPT and carries no processing result.\n",
                      "enum": [
                        true
                      ],
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Accepted for processing."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "error": {
                      "example": "scanId required",
                      "type": "string"
                    },
                    "ok": {
                      "enum": [
                        false
                      ],
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Either the text body was not parseable JSON (`{ \"ok\": false }`) or\n`scanId` was missing (`{ \"ok\": false, \"error\": \"scanId required\" }`).\n"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "error": {
                      "example": "Too many requests",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "Rate limit exceeded (60/minute)."
          }
        },
        "security": [],
        "summary": "Report QR scan device signals",
        "tags": [
          "Event Ingestion"
        ],
        "x-codeSamples": [
          {
            "label": "curl",
            "lang": "Shell",
            "source": "curl -sS -X POST \\\n  \"https://api.trillboards.com/v1/attribution/qr-signal\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"scanId\":\"<scanId>\"}'\n"
          }
        ],
        "x-trillboards-agent-safe": false,
        "x-trillboards-cost-tier": "free",
        "x-trillboards-idempotent": false,
        "x-trillboards-source": "attribution"
      }
    }
  },
  "servers": [
    {
      "description": "Production API",
      "url": "https://api.trillboards.com"
    }
  ],
  "tags": [
    {
      "description": "Per-advertiser measurement connections — the places conversion events arrive FROM. First-party portal routes, deliberately outside the external-surface gate; documented here because the credential semantics (a secret shown once, a roll with an overlap window, a URL that IS a credential) are not guessable from the request shape.",
      "name": "Measurement Sources"
    },
    {
      "description": "Campaign-level attribution summaries, timeseries, heatmaps, and journey funnels",
      "name": "Campaign Attribution"
    },
    {
      "description": "Attention-verified impression metrics from FEIN edge AI.\nOnly Trillboards provides deterministic attention data in DOOH —\nmeasured by on-device computer vision, not estimated.\n",
      "name": "Attention Metrics"
    },
    {
      "description": "Multi-touch attribution, cross-channel journeys, and per-channel ROAS across DOOH, mobile, web, and store",
      "name": "Cross-Channel Attribution"
    },
    {
      "description": "Bayesian uplift measurement with geo-holdout experiments, ghost ads, and propensity matching",
      "name": "Incrementality"
    },
    {
      "description": "Creative-level performance attribution — which creative x venue x daypart x weather combination drives the most conversions",
      "name": "Creative Attribution"
    },
    {
      "description": "Export exposed audience cohorts (MAID lists) to DSPs for retargeting activation",
      "name": "Cohort Export"
    },
    {
      "description": "Signal quality, freshness, and methodology transparency endpoints",
      "name": "Signal Transparency"
    },
    {
      "description": "Cross-channel event ingestion — DSP postbacks, conversion pixels, transaction data",
      "name": "Event Ingestion"
    },
    {
      "description": "Purchase-matched attribution — attributed revenue, ROAS, iROAS, and Bayesian sales lift from POS and server-postback conversion sources",
      "name": "Transaction & ROAS"
    },
    {
      "description": "Zero-cost audience signals — DSP-consensus segments from the bid stream, venue loyalty curves, and Census-fused demographic over-index",
      "name": "Audience Intelligence"
    },
    {
      "description": "Attribution-driven creative impression weights and venue/daypart bid modifiers, with opt-in auto-optimization",
      "name": "Optimization"
    },
    {
      "description": "Public key for signature verification (no auth required)",
      "name": "Public Key"
    },
    {
      "description": "Retrieve signed proof-of-play records",
      "name": "Proofs"
    },
    {
      "description": "Verify proof signatures",
      "name": "Verification"
    },
    {
      "description": "Batch export proofs",
      "name": "Export"
    }
  ],
  "x-trillboards-bundle": {
    "slug": "measurement",
    "sources": [
      {
        "file": "attribution-api.yaml",
        "key": "attribution"
      },
      {
        "file": "proof-of-play.yaml",
        "key": "proof"
      }
    ]
  }
}
