{"openapi":"3.1.0","info":{"title":"Event Horizon public API","version":"1","description":"Read the dashboards you share from your own code. Requests authenticate with a bearer token that identifies the dashboard, so no path contains a dashboard id. Two kinds of token work. A share link token is long lived and suits data that anyone with the link may see. For data that differs per viewer, keep an embed key on your server and create a short-lived session token for each visitor with `POST /api/v1/embed-sessions`. Only the session token reaches the browser."},"servers":[{"url":"/api/v1/public"}],"security":[{"shareToken":[]},{"embedSession":[]}],"paths":{"/dashboard":{"get":{"operationId":"getDashboard","summary":"Retrieve a dashboard","description":"Returns the dashboard, page or widget that the token gives access to, with its branding. Responses are not cached.","responses":{"200":{"description":"The shared dashboard.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed per minute for this token."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Seconds until the window resets."},"X-EH-API-Version":{"schema":{"type":"string"},"description":"The API major version that answered."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DashboardConfig"}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"The token is missing, malformed, unknown, revoked or expired, or it does not reach this resource. All of these answer identically on purpose.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The per-token request budget is spent. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The platform is shedding load. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/widgets/{widgetId}/data":{"get":{"operationId":"getWidgetData","summary":"Retrieve widget data","description":"Returns the data for one widget. Responses can be cached for 10 seconds (`Cache-Control: private, max-age=10`).","parameters":[{"name":"widgetId","in":"path","required":true,"description":"The widget id, as listed in the dashboard config.","schema":{"type":"string"}},{"name":"max_points","in":"query","description":"The most points the tile can draw; the series is thinned to fit.","schema":{"type":"integer","minimum":1,"maximum":20000}},{"name":"rows_format","in":"query","description":"`objects` (default) sends one object per row; `arrays` sends each row as an array in `columns` order, without repeating the column names.","schema":{"type":"string","enum":["objects","arrays"]}},{"name":"page[after]","in":"query","description":"Table widgets only: the `next_cursor` of the previous page. Empty for the first page.","schema":{"type":"string","maxLength":2048}},{"name":"page[size]","in":"query","description":"Table widgets only: rows per page.","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"drill","in":"query","description":"JSON: `{\"time_range\": {\"from\": \"<ISO>\", \"to\": \"<ISO>\"}, \"bucket\": \"1m\" | \"5m\" | \"1h\" | \"1d\" | \"1w\" | \"1M\" | \"1q\" | \"1y\", \"zoom\": true}`. Reads the widget over that window and grain, for a drag-to-zoom or a drill-down. With `zoom` it never reads finer than the widget's own grain and the window is rounded out to whole buckets. The token's row scope applies as always.","schema":{"type":"string"}},{"name":"cross_filters","in":"query","description":"JSON array of `{\"key\", \"operator\": \"eq\" | \"neq\" | \"like\", \"value\"}` narrowing the widget, as a click on another widget does. `value` is a string, number or boolean (a list is refused with 422). They only narrow: the token's row scope still applies.","schema":{"type":"string"}},{"name":"time","in":"query","description":"JSON, the shape of the dashboard period: `{\"preset\": \"last_7d\", \"bucket\": \"1d\"}` or `{\"from\": \"<ISO>\", \"to\": \"<ISO>\", \"bucket\": \"1d\"}`. Replaces the dashboard period for the widgets that inherit it (a widget with its own period keeps it). Only links whose `period_mode` is `open` accept it; on any other link it is refused with 422.","schema":{"type":"string"}}],"responses":{"200":{"description":"The widget data.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed per minute for this token."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Seconds until the window resets."},"X-EH-API-Version":{"schema":{"type":"string"},"description":"The API major version that answered."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WidgetData"}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"The token is missing, malformed, unknown, revoked or expired, or it does not reach this resource. All of these answer identically on purpose.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"A parameter is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"The per-token request budget is spent. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The platform is shedding load. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/widgets/{widgetId}/drill-through":{"get":{"operationId":"getDrillThrough","summary":"Retrieve widget rows","description":"Returns the underlying rows of a table widget. Returns at most 5,000 rows.","parameters":[{"name":"widgetId","in":"path","required":true,"description":"The widget id, as listed in the dashboard config.","schema":{"type":"string"}},{"name":"time_range","in":"query","required":true,"description":"JSON: `{\"from\": \"<ISO>\", \"to\": \"<ISO>\"}`.","schema":{"type":"string"}},{"name":"dimension_filters","in":"query","description":"JSON array of `{\"key\", \"operator\": \"eq\" | \"neq\" | \"like\", \"value\"}`.","schema":{"type":"string"}}],"responses":{"200":{"description":"The matching rows.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed per minute for this token."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Seconds until the window resets."},"X-EH-API-Version":{"schema":{"type":"string"},"description":"The API major version that answered."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DrillThrough"}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"The token is missing, malformed, unknown, revoked or expired, or it does not reach this resource. All of these answer identically on purpose.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"A parameter is invalid, or the widget is not a table.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"The per-token request budget is spent. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The platform is shedding load. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/widgets/{widgetId}/filter-options":{"get":{"operationId":"getFilterOptions","summary":"Retrieve the values of a filter widget","description":"Lists the distinct values of the column a filter widget offers, with their counts. Only links whose `filters_mode` is `open` serve it; on any other link, and for a widget that is not a filter, it answers 404. The token's row scope and the dashboard's fixed filters restrict the values exactly as they restrict the data.","parameters":[{"name":"widgetId","in":"path","required":true,"description":"The widget id, as listed in the dashboard config.","schema":{"type":"string"}},{"name":"filters","in":"query","description":"JSON array of `{\"key\", \"operator\": \"eq\" | \"neq\" | \"like\", \"value\"}`: the other active filters, narrowing the list.","schema":{"type":"string"}},{"name":"time_range","in":"query","description":"JSON: `{\"preset\": \"last_7d\"}` or `{\"from\": \"<ISO>\", \"to\": \"<ISO>\"}`. Absent, the period the widget would inherit. Only links whose `period_mode` is `open` accept it; on any other link it is refused with 422.","schema":{"type":"string"}}],"responses":{"200":{"description":"The values.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed per minute for this token."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests left in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Seconds until the window resets."},"X-EH-API-Version":{"schema":{"type":"string"},"description":"The API major version that answered."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/FilterOptions"}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"The token is missing, malformed, unknown, revoked or expired, or it does not reach this resource. All of these answer identically on purpose.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"A parameter is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"The per-token request budget is spent. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The platform is shedding load. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/embed-sessions":{"servers":[{"url":"/api/v1"}],"post":{"operationId":"createEmbedSession","summary":"Create an embed session","description":"Creates a short-lived session token for one visitor. Call it from your server and pass only the returned `token` to the page. The token authenticates every other operation here for `expires_in` seconds and shows what the share link shows, narrowed to `row_scope`. The embed key (`ehk_...`, created under Settings, API tokens) never leaves your server and can read no data itself. Revoking the key, or deactivating the share link, ends every session at once.","security":[{"embedKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"share_link_id":{"type":"string","description":"The id of the share link (not its token) that defines what may be read."},"row_scope":{"type":"array","maxItems":5,"description":"The viewer’s own rows, per table. Added to the link’s own scope, never replacing it: a session can only narrow what the link allows.","items":{"type":"object","properties":{"event_bucket_id":{"type":"string"},"column_key":{"type":"string"},"values":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"}}},"required":["event_bucket_id","column_key","values"],"additionalProperties":true}},"expires_in":{"type":"integer","minimum":60,"maximum":3600,"default":600,"description":"Seconds the session lives."},"subject":{"type":"string","maxLength":120,"description":"Whom you minted it for. Opaque to us; recorded in the audit trail."}},"required":["share_link_id"],"additionalProperties":false}}}},"responses":{"200":{"description":"The session.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EmbedSession"}},"required":["data"],"additionalProperties":false}}}},"401":{"description":"The embed key is missing, wrong, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The share link is not yours or is no longer active, or the body is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"Too many sessions minted for this key. Retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"shareToken":{"type":"http","scheme":"bearer","description":"A share link token. Never put it in the URL: the API refuses it there."},"embedSession":{"type":"http","scheme":"bearer","description":"A short-lived session token (`ehs_...`) from `POST /embed-sessions`. This is the one to put in a browser."},"embedKey":{"type":"http","scheme":"bearer","description":"An embed key (`ehk_...`). Server side only: it can mint sessions and read nothing."}},"schemas":{"Error":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":true},"EmbedSession":{"type":"object","properties":{"token":{"type":"string","description":"The bearer token for the page."},"expires_at":{"type":"string","description":"ISO 8601 date-time."}},"required":["token","expires_at"],"additionalProperties":true},"ValidationError":{"type":"object","properties":{"errors":{"type":"object","properties":{"message":{"type":"string"},"errors":{"type":"object","description":"Messages by parameter name.","additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["message"],"additionalProperties":true}},"required":["errors"],"additionalProperties":true},"Widget":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"title":{"type":"string"}},"required":["id","type"],"additionalProperties":true},"Branding":{"type":"object","properties":{},"required":[],"additionalProperties":true},"DashboardTime":{"type":"object","properties":{"preset":{"type":"string"},"from":{"type":"string","description":"ISO 8601 date-time."},"to":{"type":"string","description":"ISO 8601 date-time."},"bucket":{"type":"string"}},"required":["bucket"],"additionalProperties":true},"FilterOptions":{"type":"object","properties":{"values":{"type":"array","items":{"type":"object","properties":{"value":{},"count":{"type":"integer"}},"required":["value","count"],"additionalProperties":true}},"total_distinct_shown":{"type":"integer"}},"required":["values","total_distinct_shown"],"additionalProperties":true},"WidgetFilter":{"type":"object","properties":{"event_bucket_id":{"type":"string"},"key":{"type":"string"},"operator":{"type":"string","enum":["eq","neq","in","like"]},"value":{}},"required":["key","operator","value"],"additionalProperties":true},"DashboardConfig":{"type":"object","properties":{"scope":{"type":"string","enum":["dashboard","page","widget"]},"dashboard_name":{"type":"string"},"page_name":{"type":"string","description":"Page scope only."},"pages":{"type":"array","description":"Dashboard scope only.","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"order":{"type":"integer"}},"required":["id","name"],"additionalProperties":true}},"widgets":{"type":"array","description":"Dashboard and page scope.","items":{"$ref":"#/components/schemas/Widget"}},"widget":{"$ref":"#/components/schemas/Widget","description":"Widget scope only."},"branding":{"$ref":"#/components/schemas/Branding"},"time":{"$ref":"#/components/schemas/DashboardTime","description":"Present when a widget in scope inherits the dashboard period, and always when `period_mode` is `open` or `shown`."},"filters_mode":{"type":"string","enum":["locked","shown","open"],"description":"`locked`: the server applies the dashboard filters, the viewer cannot change them and the page does not show them. `shown`: the same, and `pinned_filters` is sent so the page can display them read-only. `open`: the viewer starts from `pinned_filters` and may change them."},"period_mode":{"type":"string","enum":["locked","shown","open"],"description":"`open`: the data read accepts a `time` period. `shown`: fixed, and `time` is sent so the page can display it. `locked`: fixed and not shown."},"pinned_filters":{"type":"array","description":"Only when `filters_mode` is `open` or `shown`: the dashboard's shared filters.","items":{"$ref":"#/components/schemas/WidgetFilter"}}},"required":["scope","dashboard_name","branding"],"additionalProperties":true},"Rows":{"type":"array","description":"Objects, or arrays in `columns` order when `rows_format=arrays`.","items":{"type":["object","array"]}},"WidgetData":{"type":"object","properties":{"columns":{"type":"array","items":{"type":"string"}},"column_types":{"type":"object","additionalProperties":true},"rows":{"$ref":"#/components/schemas/Rows"},"total":{"type":"integer"},"truncated":{"type":"boolean","description":"More rows exist than were returned."},"next_cursor":{"type":"string","description":"Paged table reads: pass as `page[after]`."},"effective_bucket":{"type":"string"},"requested_bucket":{"type":"string"},"requested_points":{"type":"integer"},"count_distinct_totals":{"type":"object","additionalProperties":true},"comparison_rows":{"type":"array","items":{"type":"object"}},"sparkline_rows":{"type":"array","items":{"type":"object"}},"anomaly":{"type":"object","additionalProperties":true},"forecast":{"type":"object","additionalProperties":true}},"required":["columns","column_types","rows","total"],"additionalProperties":true},"DrillThrough":{"type":"object","properties":{"columns":{"type":"array","items":{"type":"string"}},"column_types":{"type":"object","additionalProperties":true},"rows":{"type":"array","items":{"type":"object"}},"total":{"type":"integer"},"truncated":{"type":"boolean"}},"required":["columns","rows","total"],"additionalProperties":true}}}}