{"openapi":"3.1.0","info":{"title":"SurfScore External API","version":"1.1.0","description":"Read-only B2B access to SurfScore session, telemetry, sensor, and coverage data, plus webhook subscriptions for session lifecycle events. Every successful response is wrapped in an envelope carrying an `attribution` object (required on redistribution — SurfScore is a Waveriders Collective Inc. trademark) and a `meta.request_id`. Errors follow RFC 9457 (application/problem+json). Authenticate with `Authorization: Bearer ss_live_...`. Precision contract on aggregated endpoints (summary/timeline/heatmap): dB/dBm and score averages at 2 decimal places, throughput (Mbps) and capacity (MHz) at 2 dp, latency metrics as whole milliseconds, coordinates untouched; aggregate values and array ordering are deterministic — identical requests return byte-identical payloads. Operator naming contract: every operator/carrier and band value is PLMN-resolved, not the device-reported label. Operators are resolved from the (mcc, mnc) pair against the operator registry and bands from EARFCN/NRARFCN, so one operator has one spelling across all rows and endpoints. This is more precise than the device string, which is ambiguous — \"FirstNet\" is reported on both 313-100 (FirstNet) and 310-410 (AT&T commercial) — and which is sometimes a device or SIM label rather than an operator at all. Values are whitespace-trimmed. Rows with no identifiable operator or band surface as the literals \"Unknown\" and \"Unresolved\" rather than being dropped, so per-operator sample counts always reconcile against row_count. The raw mcc/mnc/earfcn/nrarfcn fields remain on telemetry rows if you want to resolve differently.","contact":{"name":"Waveriders Collective","url":"https://surfscore.live"}},"servers":[{"url":"https://api.surfscore.live","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"ss_live_<43 base64url chars>","description":"Per-organization API key. Issued and revoked from surfscore.live's account menu > API Keys page."}},"schemas":{"Problem":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]},"Attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"Meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]},"WebhookEndpoint":{"type":"object","description":"A registered webhook endpoint. The signing secret is NOT included — it is returned exactly once, by the creation call.","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["session.started","session.completed"]}},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"last_success_at":{"type":"string","format":"date-time","nullable":true},"last_failure_at":{"type":"string","format":"date-time","nullable":true},"consecutive_failures":{"type":"integer"}},"required":["id","url","events","active","created_at"]},"WebhookEventPayload":{"type":"object","description":"The JSON body POSTed to your endpoint for a lifecycle event.","properties":{"event":{"type":"string","enum":["session.started","session.completed"]},"session":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"session_mode":{"type":"string","enum":["walk_test","monitor"]},"status":{"type":"string","enum":["active","completed"]},"tenant_id":{"type":"string","format":"uuid"},"started_at":{"type":"string","format":"date-time"},"ended_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","session_mode","status","tenant_id","started_at"]}},"required":["event","session"]}}},"paths":{"/health":{"get":{"summary":"Gateway health check (public, no auth).","security":[],"responses":{"200":{"description":"Gateway is up.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"name":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}}}}}}}},"/v1/sessions":{"get":{"summary":"List sessions for your organization.","description":"Requires scope: read:sessions. Always Cache-Control: no-store.","parameters":[{"name":"mode","in":"query","schema":{"type":"string","enum":["walk_test","monitor"]}},{"name":"status","in":"query","schema":{"type":"string","enum":["calibrating","active","completed"]}},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":1000,"default":50}},{"name":"cursor","in":"query","description":"Opaque pagination cursor from a previous response's next_cursor field.","schema":{"type":"string"}}],"responses":{"200":{"description":"Sessions page.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object"}},"next_cursor":{"type":"string","nullable":true}}},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/sessions/{id}":{"get":{"summary":"Get a single session.","description":"Requires scope: read:sessions. Cacheable (ETag + max-age=3600) once the session is completed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Session.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/sessions/{id}/summary":{"get":{"summary":"Per-session rollup: row/device counts, RSRP/RSRQ/SINR and surf_score avg + median, carriers, bands.","description":"Requires scope: read:sessions. The rollup includes surf_score_avg / surf_score_p50 (computed server-side, formula named by the embedded score_version field, currently urs-v1). `carriers[]` and `bands[]` are PLMN-resolved operator and band names (see the operator naming contract in the API description), sorted ascending and distinct — so a session whose devices spell one operator several different ways yields a single entry.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Session summary.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/sessions/{id}/telemetry":{"get":{"summary":"Raw telemetry rows for a session, keyset-paginated oldest-first.","description":"Requires scope: read:telemetry. Always Cache-Control: no-store. Each row carries the full raw column set (RF metrics, QoE, GPS, thermals: temp_c, fan_speed_rpm, tx_power, location_status, signal_bars, earfcn, nrarfcn, wifi_rssi, wifi_channel, wifi_standard, wifi_vendor, interface_type) plus the server-computed `surf_score` (0-100 composite RF quality; null when the row has no RF basis). `surf_score` is distinct from `urs`, which is the raw device-reported score column. Two fields on each row are NOT raw: `carrier` and `band` are PLMN-resolved names (see the operator naming contract in the API description), not the device-reported labels. The `mcc`, `mnc`, `earfcn` and `nrarfcn` they were resolved from are returned alongside. The formula version is reported in meta.score_version (JSON) and as a top-level foreign member (GeoJSON). ?format=geojson returns a bare FeatureCollection (attribution/meta as foreign members) containing only rows with lat/lng — indoor (x/y-only) rows are omitted and counted in `omitted_indoor_count`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"since","in":"query","description":"ISO-8601 timestamp lower bound (exclusive).","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","description":"Page size.","schema":{"type":"integer","minimum":1,"maximum":1000,"default":1000}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","geojson"],"default":"json"}},{"name":"cursor","in":"query","description":"Opaque pagination cursor from a previous response's next_cursor field.","schema":{"type":"string"}}],"responses":{"200":{"description":"Telemetry page (JSON envelope, or a GeoJSON FeatureCollection when format=geojson).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"telemetry":{"type":"array","items":{"type":"object"}},"next_cursor":{"type":"string","nullable":true}}},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/sessions/{id}/timeline":{"get":{"summary":"Bucketed per-device time-series aggregate (RSRP/RSRQ/SINR/surf_score/RTT/throughput).","description":"Requires scope: read:telemetry. Each bucket includes surf_score_avg (server-computed, formula named by the top-level score_version field). bucket_seconds is clamped to >= 10; requests whose window would produce more than 5000 buckets return 400 (widen bucket_seconds or narrow from/to).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"bucket_seconds","in":"query","schema":{"type":"integer","minimum":10,"default":60}},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Bucketed timeline.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/maps/{id}/heatmap":{"get":{"summary":"Per-cell RF heatmap (floorplan pixel grid or outdoor geographic grid) over completed sessions.","description":"Requires scope: read:reports. Always Cache-Control: no-store in Phase 1. The `operator` and `band` keys on each `facets[]` entry are PLMN-resolved names (see the operator naming contract in the API description); facets are grouped on those resolved values, so one operator contributes one facet per device/modem rather than one per device-reported spelling.","parameters":[{"name":"id","in":"path","required":true,"description":"Floorplan or outdoor_map id.","schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"query","required":true,"schema":{"type":"string","enum":["floorplan","outdoor"]}},{"name":"session_ids","in":"query","required":true,"description":"Comma-separated session UUIDs.","schema":{"type":"string"}},{"name":"cell","in":"query","description":"Cell size — pixels for floorplan (default 16), meters for outdoor (default 10).","schema":{"type":"number"}}],"responses":{"200":{"description":"Heatmap cells + facets.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/scans":{"get":{"summary":"List SurfScan QSCAN scan sessions for your organization.","description":"Requires scope: read:scans. Cache-Control: no-store. Keyset-paginated newest-first; pass next_cursor back as cursor. score_version does not apply (raw listing).","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":1000,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Scan sessions + next_cursor.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/coverage/hexes":{"get":{"summary":"Org-wide H3 (res 9) coverage rollup with per-hex SurfScore.","description":"Requires scope: read:scans. Cache-Control: no-store. Data-quality corrections (mirrored-PLMN dedup, noise-operator exclusion) are applied server-side. score_version: surfscore-v1.","parameters":[{"name":"device_ids","in":"query","description":"Comma-separated device ids (default: all org devices).","schema":{"type":"string"}},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Per-hex rollups (h3_cell_9, lat/lng, surfscore, scan/session/operator counts).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/reports/operator-compare":{"get":{"summary":"Per-operator LTE/NR SurfScore + deployed-capacity comparison over selected hexes.","description":"Requires scope: read:reports. Cache-Control: no-store. Serving-grade filter (rsrp >= -85), CBRS exclusion, and mirrored-PLMN dedup applied to capacity output. score_version: surfscore-v1.","parameters":[{"name":"hex_ids","in":"query","required":true,"description":"Comma-separated h3_cell_9 ids (from /v1/coverage/hexes).","schema":{"type":"string"}},{"name":"session_ids","in":"query","description":"Comma-separated scan session ids to restrict to.","schema":{"type":"string"}}],"responses":{"200":{"description":"Per-operator scores, capacities, and carrier detail.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/reports/expert":{"get":{"summary":"Expert report: eNB/tower-grouped carrier detail over selected hexes (raw fidelity).","description":"Requires scope: read:reports. Cache-Control: no-store. RAW-data surface: mirrors the first-party expert report exactly — no serving-grade filter, no CBRS or mirrored-PLMN dedup is applied (unlike /v1/reports/operator-compare). Intended for building your own reports; apply your own filtering as needed. score_version: surfscore-v1.","parameters":[{"name":"hex_ids","in":"query","required":true,"description":"Comma-separated h3_cell_9 ids (from /v1/coverage/hexes).","schema":{"type":"string"}},{"name":"session_ids","in":"query","description":"Comma-separated scan session ids to restrict to.","schema":{"type":"string"}}],"responses":{"200":{"description":"Tower-grouped carrier detail.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/sessions/{id}/sensors":{"get":{"summary":"Per-station, per-operator sensor SurfScore rollups for a session's bound sensor devices.","description":"Requires scope: read:scans. Always Cache-Control: no-store. Stations are the SurfScan sensor devices bound to this monitor session; each carries bandwidth-weighted per-operator LTE/NR SurfScore rollups (score_version: surfscore-v1) aggregated over the binding window. Completed sessions return their historical rollup; active sessions reflect scans so far.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Sensor stations with per-operator rollups.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"session_id":{"type":"string","format":"uuid"},"session_status":{"type":"string","enum":["calibrating","active","completed"]},"generated_at":{"type":"string","format":"date-time"},"score_version":{"type":"string","example":"surfscore-v1"},"stations":{"type":"array","items":{"type":"object"}}}},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/sensors/{deviceId}/timeseries":{"get":{"summary":"Per-operator (and per-band) SurfScore time series for one sensor device.","description":"Requires scope: read:scans. Always Cache-Control: no-store. bucket is in minutes, clamped to >= 5; requests whose window would produce more than 5000 buckets return 400 (widen bucket or narrow start/end). Defaults: last 7 days, 60-minute buckets. score_version: surfscore-v1.","parameters":[{"name":"deviceId","in":"path","required":true,"description":"Sensor device id (devices.device_id).","schema":{"type":"string"}},{"name":"start","in":"query","description":"ISO-8601 window start (default: now - 7 days).","schema":{"type":"string","format":"date-time"}},{"name":"end","in":"query","description":"ISO-8601 window end (default: now).","schema":{"type":"string","format":"date-time"}},{"name":"bucket","in":"query","description":"Bucket size in minutes.","schema":{"type":"integer","minimum":5,"default":60}}],"responses":{"200":{"description":"Per-operator SurfScore series with nested per-band series.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"device_id":{"type":"string"},"device_label":{"type":"string","nullable":true},"bucket_minutes":{"type":"integer"},"window":{"type":"object","properties":{"start":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"}}},"score_version":{"type":"string","example":"surfscore-v1"},"operators":{"type":"array","items":{"type":"object"}}}},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/webhooks":{"post":{"summary":"Register a webhook endpoint for session lifecycle events.","description":"Requires scope: manage:webhooks.\n\n## Delivery\n\nWhen a session starts (`session.started`, status transition to active) or completes (`session.completed`), we POST a JSON body (schema: WebhookEventPayload) to every active endpoint subscribed to that event, with headers:\n\n- `Content-Type: application/json`\n- `X-Surfscore-Event`: the event name\n- `X-Surfscore-Delivery`: a UUID identifying this event, stable across retries — deduplicate on it, deliveries are at-least-once\n- `X-Surfscore-Signature`: `t=<unix seconds>,v1=<hex HMAC-SHA256>`\n\nRespond with any 2xx within 10 seconds to acknowledge. Anything else (including a timeout) is retried on a backoff schedule of 1m, 5m, 30m, 2h, 8h after the initial attempt — 6 attempts total, then the delivery is dropped.\n\n## Verifying signatures\n\nThe signed message is the timestamp and the RAW request body joined by a period: `<t>.<raw body bytes>`. Compute the HMAC with your endpoint secret and compare in constant time:\n\n```text\nheader    = \"t=<t>,v1=<their_hex>\"        # split on \",\" then \"=\"\nexpected  = hex( hmac_sha256( key: <your endpoint secret>,\n                              msg: \"<t>\" + \".\" + <raw body> ) )\nvalid     = constant_time_equals(expected, <their_hex>)\n            and abs(now_unix - <t>) < 300   # reject replays older than 5 min\n```\n\nVerify against the raw bytes exactly as received — re-serializing the parsed JSON can reorder keys or change whitespace and will not match.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"HTTPS delivery URL."},"events":{"type":"array","items":{"type":"string","enum":["session.started","session.completed"]},"description":"Defaults to both events when omitted."}},"required":["url"]}}}},"responses":{"201":{"description":"Endpoint created. The response includes `secret` — the HMAC signing secret — EXACTLY ONCE. Store it now; it is not retrievable afterwards.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpoint"},{"type":"object","properties":{"secret":{"type":"string","description":"whsec_-prefixed signing secret. Shown only in this response."}},"required":["secret"]}]},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}},"get":{"summary":"List registered webhook endpoints (signing secrets are never included).","description":"Requires scope: manage:webhooks.","responses":{"200":{"description":"Webhook endpoints for your organization.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"attribution":{"type":"object","properties":{"text":{"type":"string","example":"SurfScore(TM) - Waveriders Collective Inc."},"url":{"type":"string","format":"uri","example":"https://surfscore.live"}},"required":["text","url"]},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"score_version":{"type":"string","example":"urs-v1","description":"Version of the server-side surf_score formula, present on responses that carry computed scores (telemetry). Pin dashboards to a version if you care about score stability across formula revisions."}},"required":["request_id"]}},"required":["data","attribution","meta"]}}}},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}},"/v1/webhooks/{id}":{"delete":{"summary":"Delete a webhook endpoint. Pending deliveries to it are discarded.","description":"Requires scope: manage:webhooks.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted."},"400":{"description":"Bad request — malformed query parameter or cursor.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"401":{"description":"Missing, malformed, or unrecognized API key.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"403":{"description":"Key revoked/expired, api_access flag disabled, subscription inactive, or missing the required scope.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"404":{"description":"Resource does not exist, or does not belong to your organization.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"429":{"description":"Rate limit exceeded. See the Retry-After header.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}},"500":{"description":"Unexpected gateway or database error.","content":{"application/problem+json":{"schema":{"type":"object","description":"RFC 9457 problem+json error body.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"}},"required":["type","title","status"]}}}}}}}}}