diff --git a/CHANGELOG.md b/CHANGELOG.md index 450f204..e29a0f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ commands, `--json` output and exit codes. Each release's notes start with its se ## [Unreleased] +### Changed + +- The generated `/v1` client follows version 2026-10-25 of the API document. `alerts list` now + prints each alert's `updated_at` in `--json` and `--csv` output (the table is unchanged), and + `--sort` accepts `updated_at`. The `/v1/me` answer now also carries `beta` and `user`; a `null` + user (an API key) is still no user. + ([#210](https://github.com/Hodeitek/hodeishield-cli/issues/210)) + +### Fixed + +- `alerts list` no longer depends on `page`, `total` and `total_pages` in the answer, which the API + now treats as optional. When they are missing, `--all` stops at a page with a null `next_cursor` + or fewer alerts than the page size, and the `Page N of M` line is not printed. + ([#210](https://github.com/Hodeitek/hodeishield-cli/issues/210)) + ## [0.5.1] - 2026-10-18 ### Changed diff --git a/crates/hodeishield-api/src/generated.rs b/crates/hodeishield-api/src/generated.rs index 8a4f456..b4bd4eb 100644 --- a/crates/hodeishield-api/src/generated.rs +++ b/crates/hodeishield-api/src/generated.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; /// `info.version` of the OpenAPI document this client was generated from. -pub const OPENAPI_VERSION: &str = "2026-10-19"; +pub const OPENAPI_VERSION: &str = "2026-10-25"; /// The server the OpenAPI document declares. pub const DEFAULT_SERVER: &str = "https://api.hodeishield.com"; @@ -147,12 +147,32 @@ pub struct Alert { pub resolution_reason: Option, /// RFC 3339, UTC. pub created_at: String, + /// RFC 3339, UTC. Last change of severity, acknowledgement, resolution or CISA KEV status; equals the instant the service started tracking it for alerts older than that. + pub updated_at: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AlertsPagination { + /// The page that was served. + #[serde(default)] + pub page: Option, + /// Items per page in effect. + pub per_page: i64, + /// Total items matching the filters, across all pages. + #[serde(default)] + pub total: Option, + /// Number of pages at this `per_page`. + #[serde(default)] + pub total_pages: Option, + /// Pass as `cursor` for the next page; null on the last page. + #[serde(default)] + pub next_cursor: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ListAlertsResponse { pub data: Vec, - pub pagination: Pagination, + pub pagination: AlertsPagination, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -402,6 +422,28 @@ pub struct WhoAmIOrganization { pub r#type: String, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Beta { + /// The beta status of the tenant’s own organisation, or `active` while the beta of its parent MSSP runs and lifts its access. Open vocabulary: `active`, `ended` or `converted`. + pub status: String, + /// True while the beta runs: status `active` and `ends_at` in the future. While it runs the organisation has Enterprise access. + pub running: bool, + /// When the own organisation’s beta ends or ended, ISO 8601 UTC. Null when no end is recorded, and always null for a beta inherited from the parent MSSP. + #[serde(default)] + pub ends_at: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct User { + /// The user the access token belongs to (lowercase). + pub id: String, + /// The user’s display name. Untrusted text. + #[serde(default)] + pub name: Option, + /// The user’s email address. + pub email: String, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Portfolio { /// How many tenants this key can select, right now. @@ -420,6 +462,12 @@ pub struct WhoAmI { pub tenant: Option, /// The organisation of the tenant (tenant key, OAuth token) or the MSSP the key belongs to (portfolio key). pub organization: WhoAmIOrganization, + /// The beta of the tenant’s own organisation; while it does not run and the parent MSSP’s beta does, `active` and running with no date. Nothing else about the parent’s beta is described: not its end, nor whether it ended or converted. Null when the own organisation never joined the beta and no parent beta runs, and for a portfolio key. + #[serde(default)] + pub beta: Option, + /// The signed-in user of an OAuth access token (`hs_at_…`): the CALLER’S OWN id, name and email, never another user’s. Null for a tenant API key and for a portfolio key, which act for no user, and for a token whose user row is no longer visible in the token’s tenant. + #[serde(default)] + pub user: Option, /// Portfolio keys only. Null for a tenant key or an OAuth token. #[serde(default)] pub portfolio: Option, @@ -619,17 +667,20 @@ impl std::str::FromStr for BusinessCriticality { pub enum AlertSort { /// `created_at` CreatedAt, + /// `updated_at` + UpdatedAt, } impl AlertSort { /// Every accepted value, as sent on the wire. - pub const VALUES: &'static [&'static str] = &["created_at"]; + pub const VALUES: &'static [&'static str] = &["created_at", "updated_at"]; /// The value as sent on the wire. #[must_use] pub fn as_str(self) -> &'static str { match self { Self::CreatedAt => "created_at", + Self::UpdatedAt => "updated_at", } } } @@ -646,6 +697,7 @@ impl std::str::FromStr for AlertSort { fn from_str(value: &str) -> Result { match value { "created_at" => Ok(Self::CreatedAt), + "updated_at" => Ok(Self::UpdatedAt), _ => Err(crate::ParseEnumError::new(value, Self::VALUES)), } } @@ -1155,11 +1207,13 @@ impl ListVendorsParams { /// Query parameters of `listAlerts`. #[derive(Debug, Clone, Default, PartialEq)] pub struct ListAlertsParams { - /// 1-based page number. Defaults to 1. + /// 1-based page number. Defaults to 1. Cannot be combined with `cursor`. pub page: Option, /// Items per page, 1..200. Defaults to 50. pub per_page: Option, - /// Field to order by. One of: created_at. + /// Opaque keyset cursor: pass the `next_cursor` of the previous page, with the same `sort`, `order` and filters. Cannot be combined with `page`. + pub cursor: Option, + /// Field to order by. One of: created_at, updated_at. pub sort: Option, /// Sort direction. Defaults to desc. pub order: Option, @@ -1171,6 +1225,8 @@ pub struct ListAlertsParams { pub vendor_id: Option, /// Only alerts created at or after this RFC 3339 instant. pub since: Option, + /// Only alerts last changed at or after this RFC 3339 instant (`updated_at`), including ones created long before it. Poll changes with `sort=updated_at&order=asc`. + pub updated_since: Option, } impl ListAlertsParams { @@ -1184,6 +1240,9 @@ impl ListAlertsParams { if let Some(value) = &self.per_page { pairs.push(("per_page", value.to_string())); } + if let Some(value) = &self.cursor { + pairs.push(("cursor", value.clone())); + } if let Some(value) = &self.sort { pairs.push(("sort", value.as_str().to_owned())); } @@ -1202,6 +1261,9 @@ impl ListAlertsParams { if let Some(value) = &self.since { pairs.push(("since", value.clone())); } + if let Some(value) = &self.updated_since { + pairs.push(("updated_since", value.clone())); + } pairs } } @@ -1827,7 +1889,7 @@ impl crate::Client { /// List supply-chain alerts. /// - /// Alerts raised against the tenant vendors. Poll incrementally with `since` plus `order=asc`. + /// Alerts raised against the tenant vendors. Poll new alerts with `since` plus `order=asc`, changed ones with `updated_since` plus `sort=updated_at&order=asc`. /// /// Required scope: `supply_risk:read`. Published in `/v1` since 2026-09-20. /// @@ -2037,7 +2099,7 @@ impl crate::Client { /// Describe the calling credential. /// - /// Who the presented credential is: its type and scopes, and the tenant and organisation it reads (a tenant API key or an OAuth token), or the MSSP organisation it belongs to and how many tenants it can reach (a portfolio key; the tenants themselves are on `GET /v1/portfolio/tenants`). It describes the caller only: no other tenant, no secret, never the key or its hash. Any valid credential may call it: it requires NO scope, and it uses the same authentication and the same rate limits as every other operation. It never takes `X-HS-Tenant`. + /// Who the presented credential is: its type and scopes, and the tenant and organisation it reads (a tenant API key or an OAuth token), for an OAuth token also the signed-in user (`user`: its own id, name and email, never another user’s; null for any key), or the MSSP organisation it belongs to and how many tenants it can reach (a portfolio key; the tenants themselves are on `GET /v1/portfolio/tenants`). It describes the caller only: no other tenant, no secret, never the key or its hash. Any valid credential may call it: it requires NO scope, and it uses the same authentication and the same rate limits as every other operation. It never takes `X-HS-Tenant`. /// /// Required scope: `none (any valid credential)`. Published in `/v1` since 2026-10-17. /// diff --git a/crates/hodeishield-cli/src/commands/resources.rs b/crates/hodeishield-cli/src/commands/resources.rs index c0c03a6..d4efbf5 100644 --- a/crates/hodeishield-cli/src/commands/resources.rs +++ b/crates/hodeishield-cli/src/commands/resources.rs @@ -19,9 +19,10 @@ use crate::output::{ use hodeishield_api::Client; use hodeishield_api::portfolio::Tenant; use hodeishield_api::v1::{ - Alert, ComplianceControl, DownloadReportParams, Endpoint, Evidence, Format, ListAlertsParams, - ListComplianceControlsParams, ListEndpointsParams, ListEvidenceParams, ListReportsParams, - ListRisksParams, ListVendorsParams, Pagination, Report, Risk, Vendor, operations, + Alert, AlertsPagination, ComplianceControl, DownloadReportParams, Endpoint, Evidence, Format, + ListAlertsParams, ListComplianceControlsParams, ListEndpointsParams, ListEvidenceParams, + ListReportsParams, ListRisksParams, ListVendorsParams, Pagination, Report, Risk, Vendor, + operations, }; use hodeishield_api::{ApiResponse, Error}; use serde::Serialize; @@ -44,6 +45,58 @@ const MAX_PAGES: i64 = 1_000; /// hundred MiB at most. const MAX_ITEMS: usize = 100_000; +/// What the CLI needs from a list's `pagination`. Alerts may omit the page counts (`page`, `total`, +/// `total_pages`) and report `next_cursor` instead; every other list always sends the counts. +pub(super) struct PageInfo { + pub page: Option, + pub per_page: i64, + pub total: Option, + pub total_pages: Option, + pub next_cursor: Option, +} + +impl From for PageInfo { + fn from(p: Pagination) -> Self { + Self { + page: Some(p.page), + per_page: p.per_page, + total: Some(p.total), + total_pages: Some(p.total_pages), + next_cursor: None, + } + } +} + +impl From for PageInfo { + fn from(p: AlertsPagination) -> Self { + Self { + page: p.page, + per_page: p.per_page, + total: p.total, + total_pages: p.total_pages, + next_cursor: p.next_cursor, + } + } +} + +impl PageInfo { + /// True when `batch_len` items asked for as page `page` were the last ones. Uses the counts when + /// the server sent them; without them, a short page or a null `next_cursor` ends the walk. + fn is_last(&self, page: i64, batch_len: usize) -> bool { + match (self.total_pages, self.total) { + (Some(total_pages), Some(total)) => { + page >= total_pages + || self.page != Some(page) + || page.saturating_mul(self.per_page.max(1)) >= total + } + _ => { + self.next_cursor.is_none() + || i64::try_from(batch_len).is_ok_and(|n| n < self.per_page) + } + } + } +} + /// Prints a list for the selected client, or for every client with `--all-tenants`. #[allow(clippy::too_many_arguments)] fn list( @@ -54,7 +107,7 @@ fn list( api: ApiContext<'_>, targets: &[Target], fetch: &mut FetchFrom<'_, R>, - split: fn(R) -> (Vec, Pagination), + split: fn(R) -> (Vec, PageInfo), render: fn(&[T]) -> Rows, ) -> Result<()> { if matches!(ctx.selection, Selection::All) { @@ -82,7 +135,7 @@ pub(super) fn list_one( noun: &str, api: ApiContext<'_>, fetch: &mut Fetch<'_, R>, - split: fn(R) -> (Vec, Pagination), + split: fn(R) -> (Vec, PageInfo), render: fn(&[T]) -> Rows, ) -> Result<()> { if !paging.all { @@ -101,16 +154,17 @@ pub(super) fn list_one( } else { print_table(out, &render(&items).into_table())?; } - if pagination.total_pages > 1 { - let next = if pagination.page < pagination.total_pages { - format!(" Next: --page {}, or --all.", pagination.page + 1) + // Without the page counts there is no footer to print: only `--all` knows where it ends. + if let (Some(page), Some(total_pages), Some(total)) = + (pagination.page, pagination.total_pages, pagination.total) + && total_pages > 1 + { + let next = if page < total_pages { + format!(" Next: --page {}, or --all.", page + 1) } else { String::new() }; - eprintln!( - "Page {} of {} ({} {noun}).{next}", - pagination.page, pagination.total_pages, pagination.total - ); + eprintln!("Page {page} of {total_pages} ({total} {noun}).{next}"); } return Ok(()); } @@ -149,7 +203,7 @@ fn list_across( api: ApiContext<'_>, targets: &[Target], fetch: &mut FetchFrom<'_, R>, - split: fn(R) -> (Vec, Pagination), + split: fn(R) -> (Vec, PageInfo), render: fn(&[T]) -> Rows, ) -> Result<()> { let per_page = paging.per_page.or(Some(200)); @@ -275,7 +329,7 @@ pub(super) fn collect_pages( per_page: Option, max_items: usize, fetch: &mut Fetch<'_, R>, - split: fn(R) -> (Vec, Pagination), + split: fn(R) -> (Vec, PageInfo), ) -> Result<(Vec, Vec)> { let mut raw_items = Vec::new(); let mut items = Vec::new(); @@ -287,10 +341,7 @@ pub(super) fn collect_pages( } let (mut batch, pagination) = split(response.data); // Stop on whichever says "last page" first, and never trust the server to end the walk. - let done = batch.is_empty() - || page >= pagination.total_pages - || pagination.page != page - || page.saturating_mul(pagination.per_page.max(1)) >= pagination.total; + let done = batch.is_empty() || pagination.is_last(page, batch.len()); items.append(&mut batch); if items.len() > max_items { return Err(crate::failure::Failure::general(format!( @@ -363,7 +414,7 @@ pub fn vendors(ctx: &Context, command: VendorsCommand, out: &mut dyn Write) -> R ..base.clone() }) }, - |r| (r.data, r.pagination), + |r| (r.data, r.pagination.into()), vendor_table, ) } @@ -427,7 +478,7 @@ pub fn alerts(ctx: &Context, command: AlertsCommand, out: &mut dyn Write) -> Res ..base.clone() }) }, - |r| (r.data, r.pagination), + |r| (r.data, r.pagination.into()), alert_table, ) } @@ -494,7 +545,7 @@ pub fn risks(ctx: &Context, command: RisksCommand, out: &mut dyn Write) -> Resul ..base.clone() }) }, - |r| (r.data, r.pagination), + |r| (r.data, r.pagination.into()), risk_table, ) } @@ -555,7 +606,7 @@ pub fn compliance(ctx: &Context, command: ComplianceCommand, out: &mut dyn Write ..base.clone() }) }, - |r| (r.data, r.pagination), + |r| (r.data, r.pagination.into()), control_table, ) } @@ -636,7 +687,7 @@ pub fn evidence(ctx: &Context, command: EvidenceCommand, out: &mut dyn Write) -> ..base.clone() }) }, - |r| (r.data, r.pagination), + |r| (r.data, r.pagination.into()), evidence_table, ) } @@ -697,7 +748,7 @@ pub fn endpoints(ctx: &Context, command: EndpointsCommand, out: &mut dyn Write) ..base.clone() }) }, - |r| (r.data, r.pagination), + |r| (r.data, r.pagination.into()), endpoint_table, ) } @@ -764,7 +815,7 @@ pub fn reports(ctx: &Context, command: ReportsCommand, out: &mut dyn Write) -> R ..base.clone() }) }, - |r| (r.data, r.pagination), + |r| (r.data, r.pagination.into()), report_table, ) } @@ -910,20 +961,35 @@ mod tests { raw: serde_json::json!({ "data": batch }), data: ( batch, - Pagination { - page, + PageInfo { + page: Some(page), per_page, - total, - total_pages, + total: Some(total), + total_pages: Some(total_pages), + next_cursor: None, }, ), meta: Default::default(), }) }, - |r: (Vec, Pagination)| r, + |r: (Vec, PageInfo)| r, ) } + #[test] + fn without_page_counts_a_short_page_or_a_null_cursor_ends_the_walk() { + let info = |per_page, next_cursor: Option<&str>| PageInfo { + page: None, + per_page, + total: None, + total_pages: None, + next_cursor: next_cursor.map(str::to_owned), + }; + assert!(!info(2, Some("c")).is_last(1, 2)); + assert!(info(2, Some("c")).is_last(1, 1)); + assert!(info(2, None).is_last(1, 2)); + } + #[test] fn a_walk_within_the_item_limit_collects_everything() { let (raw, items) = walk(10, 4, 10).expect("within the limit"); diff --git a/crates/hodeishield-cli/src/commands/session.rs b/crates/hodeishield-cli/src/commands/session.rs index f9591ea..7acea69 100644 --- a/crates/hodeishield-cli/src/commands/session.rs +++ b/crates/hodeishield-cli/src/commands/session.rs @@ -243,8 +243,8 @@ pub fn whoami(ctx: &Context, out: &mut dyn Write) -> Result<()> { ) } -/// What `GET /v1/me` says about the credential: the generated type, and the signed-in user, which -/// the document of the API does not describe yet and so is read from the body as received. +/// What `GET /v1/me` says about the credential: the generated type, and the signed-in user, read +/// again from the body as received so that a user with missing fields still shows what it has. struct Me { who: WhoAmI, user: Option, @@ -985,8 +985,8 @@ mod tests { #[test] fn whoami_without_a_user_from_the_api_falls_back_to_userinfo() { - // Today's API (no `user` field), `user: null`, and a malformed `user`. - for user in [None, Some("null"), Some(r#""x""#), Some("[]"), Some("{}")] { + // An answer with no `user` field, and `user: null` (what an API key gets). + for user in [None, Some("null")] { let (text, printed) = whoami_of_a_sign_in(user, Some("Old Way ")); assert!( text.contains("User Old Way \n"), @@ -998,7 +998,7 @@ mod tests { #[test] fn whoami_without_any_user_has_no_user_line_and_does_not_fail() { - for user in [None, Some("null"), Some(r#""x""#), Some("42"), Some("{}")] { + for user in [None, Some("null")] { let (text, printed) = whoami_of_a_sign_in(user, None); assert!(!text.contains("User"), "{user:?}: {text}"); assert_eq!(printed["user"], Value::Null, "{user:?}"); @@ -1020,8 +1020,16 @@ mod tests { #[test] fn whoami_uses_the_id_when_the_api_gives_no_name_or_email() { - let (text, _) = whoami_of_a_sign_in(Some(r#"{"id":"u-1","name":null}"#), None); - assert!(text.contains("User u-1\n"), "{text}"); + // Off the contract (`email` is required), so it is read from the raw body, not the typed one. + let user = MeUser::from_raw(&json!({"id": "u-1", "name": null})).expect("a user"); + assert_eq!(user.label().as_deref(), Some("u-1")); + } + + #[test] + fn a_user_that_is_not_an_object_with_a_field_is_no_user() { + for raw in [json!("x"), json!([]), json!(42), json!({}), Value::Null] { + assert!(MeUser::from_raw(&raw).is_none(), "{raw}"); + } } #[test] diff --git a/crates/hodeishield-cli/src/commands/tenants.rs b/crates/hodeishield-cli/src/commands/tenants.rs index 2fccf4e..0bb8b34 100644 --- a/crates/hodeishield-cli/src/commands/tenants.rs +++ b/crates/hodeishield-cli/src/commands/tenants.rs @@ -144,7 +144,7 @@ fn list_portfolio( totals.push(response.data.pagination.total); Ok(response) }, - |r: ListTenantsResponse| (r.data, r.pagination), + |r: ListTenantsResponse| (r.data, r.pagination.into()), ) .map_err(|failure| { if failure.kind == Kind::Forbidden { @@ -218,7 +218,7 @@ pub fn run(ctx: &Context, command: TenantsCommand, out: &mut dyn Write) -> Resul is_portfolio_key: is_portfolio, }, &mut |page, per_page| client.list_tenants(&ListTenantsParams::alphabetical(page, per_page)), - |r| (r.data, r.pagination), + |r| (r.data, r.pagination.into()), tenant_rows, ) .map_err(|failure| { diff --git a/crates/hodeishield-cli/tests/cli.rs b/crates/hodeishield-cli/tests/cli.rs index fbd1223..f311895 100644 --- a/crates/hodeishield-cli/tests/cli.rs +++ b/crates/hodeishield-cli/tests/cli.rs @@ -237,7 +237,7 @@ fn all_walks_every_page() { mockito::Matcher::UrlEncoded("per_page".into(), "200".into()), ])) .with_body(format!( - r#"{{"data":[{{"id":"a{n}","vendor_id":"v","type":"cve","severity":null,"title":"{name}","description":null,"source_url":null,"economic_impact":null,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z"}}],"pagination":{{"page":{n},"per_page":1,"total":2,"total_pages":2}}}}"# + r#"{{"data":[{{"id":"a{n}","vendor_id":"v","type":"cve","severity":null,"title":"{name}","description":null,"source_url":null,"economic_impact":null,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z"}}],"pagination":{{"page":{n},"per_page":1,"total":2,"total_pages":2}}}}"# )) .expect(1) .create(); @@ -258,6 +258,56 @@ fn all_walks_every_page() { assert_eq!(titles, ["First", "Second"]); } +/// An alerts answer in the 2026-10-25 shape: no `page`, `total` or `total_pages`, only a cursor. +fn cursor_page(n: i64, next_cursor: Option<&str>) -> String { + let next = next_cursor.map_or("null".to_owned(), |c| format!(r#""{c}""#)); + format!( + r#"{{"data":[{{"id":"a{n}","vendor_id":"v","type":"cve","severity":null,"title":"T{n}","description":null,"source_url":null,"economic_impact":null,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z"}}],"pagination":{{"per_page":1,"next_cursor":{next}}}}}"# + ) +} + +#[test] +fn alerts_without_page_counts_list_without_a_footer() { + let env = Env::new(); + let mut server = mockito::Server::new(); + server + .mock("GET", "/v1/alerts") + .match_query(mockito::Matcher::Any) + .with_body(cursor_page(1, Some("c1"))) + .create(); + let output = env + .api(&server) + .args(["alerts", "list"]) + .output() + .expect("run"); + assert!(output.status.success(), "{output:?}"); + assert!(String::from_utf8_lossy(&output.stdout).contains("T1")); + assert!(!String::from_utf8_lossy(&output.stderr).contains("Page ")); +} + +#[test] +fn all_without_page_counts_stops_at_a_null_cursor() { + let env = Env::new(); + let mut server = mockito::Server::new(); + for (n, next) in [(1, Some("c1")), (2, None)] { + server + .mock("GET", "/v1/alerts") + .match_query(mockito::Matcher::UrlEncoded("page".into(), n.to_string())) + .with_body(cursor_page(n, next)) + .expect(1) + .create(); + } + let output = env + .api(&server) + .args(["alerts", "list", "--all", "--json"]) + .output() + .expect("run"); + assert!(output.status.success(), "{output:?}"); + let items: serde_json::Value = serde_json::from_slice(&output.stdout).expect("json"); + assert_eq!(items.as_array().expect("array").len(), 2); + assert_eq!(items[1]["updated_at"], "2026-01-02T00:00:00Z"); +} + #[test] fn a_missing_scope_exits_4_and_names_the_framework_scope() { let env = Env::new(); @@ -724,7 +774,7 @@ fn csv_with_all_prints_one_header_and_every_page() { .mock("GET", "/v1/alerts") .match_query(mockito::Matcher::UrlEncoded("page".into(), n.to_string())) .with_body(format!( - r#"{{"data":[{{"id":"a{n}","vendor_id":"v","type":"cve","severity":null,"title":"{title}","description":null,"source_url":null,"economic_impact":-5,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z"}}],"pagination":{{"page":{n},"per_page":1,"total":2,"total_pages":2}}}}"# + r#"{{"data":[{{"id":"a{n}","vendor_id":"v","type":"cve","severity":null,"title":"{title}","description":null,"source_url":null,"economic_impact":-5,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z"}}],"pagination":{{"page":{n},"per_page":1,"total":2,"total_pages":2}}}}"# )) .expect(1) .create(); @@ -738,9 +788,9 @@ fn csv_with_all_prints_one_header_and_every_page() { assert_eq!( String::from_utf8_lossy(&output.stdout), "id,vendor_id,type,severity,title,description,source_url,economic_impact,status,\ - acknowledged_at,resolved_at,resolution_reason,created_at\r\n\ - a1,v,cve,,'=cmd(),,,-5,open,,,,2026-01-01T00:00:00Z\r\n\ - a2,v,cve,,\"Second, with a comma\",,,-5,open,,,,2026-01-01T00:00:00Z\r\n" + acknowledged_at,resolved_at,resolution_reason,created_at,updated_at\r\n\ + a1,v,cve,,'=cmd(),,,-5,open,,,,2026-01-01T00:00:00Z,2026-01-02T00:00:00Z\r\n\ + a2,v,cve,,\"Second, with a comma\",,,-5,open,,,,2026-01-01T00:00:00Z,2026-01-02T00:00:00Z\r\n" ); assert!(String::from_utf8_lossy(&output.stderr).contains("2 alerts.")); } diff --git a/crates/hodeishield-cli/tests/contract.rs b/crates/hodeishield-cli/tests/contract.rs index 3f2974b..059541a 100644 --- a/crates/hodeishield-cli/tests/contract.rs +++ b/crates/hodeishield-cli/tests/contract.rs @@ -259,8 +259,8 @@ const RESOURCES: [Resource; 6] = [ command: &["alerts"], list_args: &[], path: "/v1/alerts", - item: r#"{"id":"a1","vendor_id":"v1","type":"cve","severity":"high","title":"Example alert","description":null,"source_url":null,"economic_impact":null,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z","added_later":"x"}"#, - csv_header: "id,vendor_id,type,severity,title,description,source_url,economic_impact,status,acknowledged_at,resolved_at,resolution_reason,created_at,added_later", + item: r#"{"id":"a1","vendor_id":"v1","type":"cve","severity":"high","title":"Example alert","description":null,"source_url":null,"economic_impact":null,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z","added_later":"x"}"#, + csv_header: "id,vendor_id,type,severity,title,description,source_url,economic_impact,status,acknowledged_at,resolved_at,resolution_reason,created_at,updated_at,added_later", }, Resource { command: &["risks"], @@ -634,8 +634,8 @@ fn csv_document_snapshot() { .match_query(mockito::Matcher::Any) .with_body( r#"{"data":[ -{"id":"a1","vendor_id":"v","type":"cve","severity":"+high","title":"=1+1","description":"-x","source_url":"@SUM(A1)","economic_impact":-5,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z"}, -{"id":"a2","vendor_id":"v","type":"cve","severity":"say \"hi\"","title":"plain, with comma","description":"two\nlines","source_url":" padded ","economic_impact":2.5,"status":"a=b","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z"} +{"id":"a1","vendor_id":"v","type":"cve","severity":"+high","title":"=1+1","description":"-x","source_url":"@SUM(A1)","economic_impact":-5,"status":"open","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z"}, +{"id":"a2","vendor_id":"v","type":"cve","severity":"say \"hi\"","title":"plain, with comma","description":"two\nlines","source_url":" padded ","economic_impact":2.5,"status":"a=b","acknowledged_at":null,"resolved_at":null,"resolution_reason":null,"created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-02T00:00:00Z"} ],"pagination":{"page":1,"per_page":50,"total":2,"total_pages":1}}"#, ) .create(); @@ -644,9 +644,9 @@ fn csv_document_snapshot() { assert_eq!( out(&output), "id,vendor_id,type,severity,title,description,source_url,economic_impact,status,\ - acknowledged_at,resolved_at,resolution_reason,created_at\r\n\ - a1,v,cve,'+high,'=1+1,'-x,'@SUM(A1),-5,open,,,,2026-01-01T00:00:00Z\r\n\ - a2,v,cve,\"say \"\"hi\"\"\",\"plain, with comma\",\"two\nlines\",\" padded \",2.5,a=b,,,,2026-01-01T00:00:00Z\r\n" + acknowledged_at,resolved_at,resolution_reason,created_at,updated_at\r\n\ + a1,v,cve,'+high,'=1+1,'-x,'@SUM(A1),-5,open,,,,2026-01-01T00:00:00Z,2026-01-02T00:00:00Z\r\n\ + a2,v,cve,\"say \"\"hi\"\"\",\"plain, with comma\",\"two\nlines\",\" padded \",2.5,a=b,,,,2026-01-01T00:00:00Z,2026-01-02T00:00:00Z\r\n" ); } diff --git a/openapi/README.md b/openapi/README.md index 6d094cb..5a75b2a 100644 --- a/openapi/README.md +++ b/openapi/README.md @@ -6,8 +6,8 @@ from it; nothing in that client is written by hand from memory. | Field | Value | |---|---| -| `info.version` | `2026-10-19` | -| SHA-256 | `8c9a44bc696584105f10b67d490a8816dabc26b07b80ed18369e6d3e498359c4` | +| `info.version` | `2026-10-25` | +| SHA-256 | `26e5a79d4669eaa3394c9917e66d56af546fc861d6576af1d6caada37c9c3458` | | Operations | 16, all `GET` | ## Updating it diff --git a/openapi/v1.json b/openapi/v1.json index 3dda78e..bc251a2 100644 --- a/openapi/v1.json +++ b/openapi/v1.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Hodei-Shield API","version":"2026-10-19","summary":"Third-party risk and compliance posture, for your own systems.","description":"## Versioning\n\n`/v1` is frozen. These changes are **compatible** and will happen inside it\nwithout notice:\n\n- a new operation;\n- a new **optional** query parameter;\n- a new field in a response object;\n- a new value in a field documented as an open vocabulary (`status`,\n `severity`, `evidence_type`, the keys of `by_status`);\n- a new `error.code`, always with the HTTP status its siblings already use;\n- anything under `error.details`, which is diagnostic and not part of the\n contract — except `error.details.code` and the keys the error table of\n the operation names (`missing`, `licence`, `parameter`, `scope`,\n `limit`, `window_seconds`, `retry_after_seconds`, `status`,\n `formats`, `limit_bytes`): those are contract.\n `error.details.code` is an OPEN vocabulary: a value, once published, keeps\n its meaning and the HTTP status it is sent with; new values may be added.\n\nWrite your client so those do not break it: ignore unknown fields, do not\nswitch exhaustively on a documented-open vocabulary, and treat an unknown\n`error.code` as \"an error of this HTTP status\".\n\nThese changes are **incompatible** and will NOT happen inside `/v1`. They go\nto `/v2`, and `/v1` keeps serving:\n\n- removing or renaming an operation, a parameter or a field;\n- changing the type, the unit or the meaning of a field;\n- making an optional parameter required, or narrowing what one accepts;\n- changing which scope an operation requires;\n- changing the HTTP status an existing `error.code` is sent with.\n\n## Read-only\n\n`/v1` only reads. Every operation is a `GET`. Any other method (`POST`, `PUT`,\n`PATCH`, `DELETE`) on a path that exists answers **405** with `Allow: GET`, and on\na path that does not exist answers **404**: neither changes any state. A write\nwill be offered only as a new operation, which is a compatible change, once a\ncustomer asks for a concrete one.\n\n## Downloads\n\nAn operation whose success is a FILE (`GET /v1/reports/{id}/download`) answers\n**200** with the bytes, the `Content-Type` of the format asked for and\n`Content-Disposition: attachment; filename=\"…\"`; it never answers a URL to fetch\nthem from. Its errors are the same JSON envelope as everywhere else. A file that\ncannot be served as asked is a **409** with `error.details.code`: the report is\nnot ready yet (`REPORT_NOT_READY`), its content has no renderer for that format\n(`FORMAT_UNSUPPORTED`) or the file exceeds the size the operation serves\n(`FILE_TOO_LARGE`, with `limit_bytes`). Every file delivered is recorded in the\ntenant's audit trail with the credential that received it.\n\n## Authentication\n\nEvery operation takes `Authorization: Bearer `. Three credentials\nexist:\n\n- a **tenant API key** (`hsk_…`): belongs to ONE tenant and carries an\n explicit list of scopes; it can read nothing outside that tenant, whatever it\n asks for;\n- a user's **OAuth access token** (`hs_at_…`): one user, ONE tenant chosen\n at consent, read scopes only;\n- an MSSP **portfolio key** (`hsp_…`): held by a managed security service\n provider, it reads ANY of its client tenants, ONE per request, selected with\n the request header `X-HS-Tenant: ` (a UUID, never a slug).\n `GET /v1/portfolio/tenants` lists the tenants it may select. Read-only, with\n a mandatory expiry. A tenant outside the key's portfolio answers **404**\n (`error.details.code: \"TENANT_NOT_FOUND\"`), exactly as one that does not\n exist.\n\nEvery successful (2xx) answer of a data operation carries `X-HS-Tenant` with\nthe id of the tenant whose data the body holds, for every credential; no error\ncarries it. A tenant API key or OAuth token may send `X-HS-Tenant` only with\nits own tenant's id; any other value is a **400**\n(`TENANT_SELECTOR_NOT_SUPPORTED`).\n\nEach operation below declares the scope it requires. A credential that\nauthenticates but lacks the scope is answered **403**\n(`error.details.code: \"INSUFFICIENT_SCOPE\"`), never 404: you are told what you\nare missing, not that the resource does not exist.\n\n## Errors\n\nEvery error has the same body:\n\n```json\n{ \"error\": { \"code\": \"forbidden\", \"message\": \"…\", \"request_id\": \"…\" } }\n```\n\nBranch on `code`, log `request_id`, show `message` to nobody but yourself.\n`request_id` is also the `X-Request-Id` response header, and it is what\nsupport will ask you for.\n\n## Rate limits\n\nEvery response that got past authentication carries the limit that is closest\nto being exhausted:\n\n- `RateLimit-Limit` — requests allowed in the window;\n- `RateLimit-Remaining` — requests left in the current window;\n- `RateLimit-Reset` — **seconds** until the window renews;\n- `RateLimit-Policy` — `;w=`.\n\nThree limits apply, each per one-minute window: per API key, per organisation\n(all of its keys together) and per service. The first two come from the licence\nof the organisation the key belongs to: a higher licence tier has more room, and\nan MSSP partner has more than a direct customer of the same tier. Read the\ncurrent limit from `RateLimit-Limit` and `RateLimit-Policy` rather than\nhard-coding it. When one is exhausted the answer is\n**429** with `error.code: \"rate_limited\"`, `Retry-After` (seconds) and\n`error.details.scope` (`key`, `tenant` or `service`) saying which. A\nportfolio key is limited per key (`portfolio_key`), per provider across all\nits portfolio keys (`portfolio_org`) and per service; once the selected tenant\nis known to be in its portfolio, the client tenant's own limit (`tenant`) and\nthe provider's share of it (`portfolio_tenant`) apply too. Your\norganisation's usage never counts against another's. Windows are fixed, so a\nclient can burst up to twice the limit across a window boundary: back off on\n`Retry-After` rather than pacing to the second. The limits are part of the\nservice, not of the frozen contract: they may be raised without notice.\n\n## Pagination, filtering and ordering\n\nEvery collection takes `page` and `per_page` (max 200) and answers with a\n`pagination` object carrying `total` and `total_pages`. Every collection\ntakes `sort` (an enum of the fields THAT collection can order by) and\n`order` (`asc`/`desc`, default `desc`). Filters are exact matches unless\nthe parameter says otherwise. A parameter or field with a known vocabulary lists\nit in `x-hs-known-values` (an OpenAPI extension, not a JSON Schema `enum`: the\nserver still accepts what it accepted before). Values retired from a parameter\nare listed in `x-hs-deprecated-values`, with the date until which the server is\ndocumented to accept them in `x-hs-deprecated-values-sunset`.\n\n## Contract changes\n\nCorrections and additions to the published contract, newest first. They ship\nwith the contract version (`info.version`) named in each entry. Detect a\ncapability by the operation, not by this string: the portfolio contract is\npresent when `GET /v1` lists `listPortfolioTenants`.\n\n- **2026-10-19 — addition: reports.** `GET /v1/reports` (`listReports`),\n `GET /v1/reports/{id}` (`getReport`) and\n `GET /v1/reports/{id}/download` (`downloadReport`), all under the scope\n `reports:read`. The download is the contract's first non-JSON success: a\n PDF or XLSX file as an attachment (see Downloads). A new `error.code`,\n `conflict` (**409**), answered only by the download, with three new\n `error.details.code` values (`REPORT_NOT_READY`, `FORMAT_UNSUPPORTED`,\n `FILE_TOO_LARGE`). Nothing existing was removed, renamed or re-statused.\n\n- **2026-10-18 — addition: known values for four filters.** `GET /v1/risks`\n `domain` and `status`, `GET /v1/compliance/controls` `status` and the\n `vendor_type` field of `GET /v1/vendors` now list their vocabulary in\n `x-hs-known-values`, taken from the lists the platform validates against.\n They stay open: a value outside the list is still accepted and matches exactly,\n as before. `GET /v1/vendors` `category` is free text with no closed\n vocabulary, and its description now says so. Nothing existing was removed,\n renamed or re-statused.\n\n- **2026-10-17 — addition: `GET /v1/me`.** The operation `whoAmI` describes the\n calling credential: its type and scopes, the tenant (id, name) and organisation it\n reads, or, for a portfolio key, the MSSP organisation and how many tenants it can\n reach. Any valid credential may call it and it requires no scope; it uses the same\n authentication and rate limits as every other operation, and refuses\n `X-HS-Tenant`. Nothing existing was removed, renamed or re-statused.\n\n- **2026-10-16 — addition: MSSP portfolio keys.** A third credential, the\n portfolio key (`hsp_…`); the operation `GET /v1/portfolio/tenants`\n (`listPortfolioTenants`, scope `portfolio:read`); the optional request\n header `X-HS-Tenant` on every data operation, echoed on every 2xx answer of\n them for every credential, with `Vary: X-HS-Tenant`;\n `error.details.code` becomes contract (`TENANT_HEADER_INVALID`,\n `TENANT_REQUIRED`, `TENANT_SELECTOR_NOT_SUPPORTED`, `TENANT_NOT_FOUND`,\n `INSUFFICIENT_SCOPE`, `LICENCE_INACTIVE`), and every scope 403 now carries\n `INSUFFICIENT_SCOPE`; three new 429 `details.scope` values\n (`portfolio_key`, `portfolio_org`, `portfolio_tenant`). Nothing existing\n was removed, renamed or re-statused.\n\n- **2026-10-16 — correction: alert `severity` is English.** `GET /v1/alerts`\n and `GET /v1/alerts/{id}` returned `severity` as stored, in Spanish\n (`critico`, `alto`, `medio`, `bajo`, `info`), while this document\n promised `critical | high | medium | low`. They now return `critical`,\n `high`, `medium`, `low` and `info` (`info` is added to the documented\n values). **This changes what an integrator who adapted to the Spanish values\n receives.** The `severity` filter accepts both the English values and the\n Spanish ones; the Spanish ones are documented as accepted until\n **2027-01-14** (90 days after this version), so switch to the English ones\n before then. A value outside both lists is still matched exactly and matches\n nothing. `GET /v1/risks`: the descriptions of `domain` and `status` named\n values that do not exist; they now list the real ones (no value changed).","contact":{"name":"Hodeitek S.L.","url":"https://hodeishield.com"}},"servers":[{"url":"https://api.hodeishield.com"}],"tags":[{"name":"vendors"},{"name":"alerts"},{"name":"risks"},{"name":"compliance"},{"name":"evidence"},{"name":"endpoints"},{"name":"Portfolio"},{"name":"Identity"},{"name":"reports"}],"paths":{"/v1/vendors":{"get":{"operationId":"listVendors","summary":"List third parties","description":"The tenant third-party inventory, newest first by default. Soft-deleted vendors are never returned.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["vendors"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, updated_at, name.","schema":{"default":"created_at","type":"string","enum":["created_at","updated_at","name"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring of the name or the domain.","schema":{"type":"string","minLength":1,"maxLength":120}},{"name":"category","in":"query","required":false,"description":"Exact category match, as the tenant wrote it (case-sensitive). Category is free text with NO closed vocabulary, so there is no list of values to publish: read `category` from `GET /v1/vendors`. To filter by the kind of third party, read `vendor_type` (`x-hs-known-values`).","schema":{"type":"string","minLength":1,"maxLength":120}},{"name":"active","in":"query","required":false,"description":"Filter by the operational flag.","schema":{"type":"string"}},{"name":"business_criticality","in":"query","required":false,"description":"Exact criticality match.","schema":{"type":"string","enum":["low","medium","high","critical"]}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Stable identifier of the vendor in this tenant."},"name":{"type":"string","description":"Display name."},"legal_name":{"description":"Registered legal name, when it differs.","type":["string","null"]},"domain":{"description":"Primary domain, lowercase, no scheme.","type":["string","null"]},"category":{"description":"Free-text category assigned by the tenant. NOT a closed vocabulary: the console form stores the label of the tenant’s language, and the API accepts any text. List the vendors to see the values a tenant uses.","type":["string","null"]},"vendor_type":{"description":"Kind of third party: software | cloud | hardware | service_provider. Open vocabulary: a new kind is a compatible change.","x-hs-known-values":["software","cloud","hardware","service_provider"],"type":["string","null"]},"access_level":{"description":"Level of access the vendor has to the tenant systems.","type":["string","null"]},"business_criticality":{"description":"low | medium | high | critical, as set by the tenant.","type":["string","null"]},"data_sensitivity":{"description":"public | internal | confidential | restricted.","type":["string","null"]},"economic_impact":{"description":"Estimated economic impact of an outage, in the tenant currency.","type":["number","null"]},"active":{"type":"boolean","description":"Whether the vendor is operationally enabled."},"monitoring_status":{"description":"Continuous-monitoring state.","type":["string","null"]},"last_scan_at":{"description":"RFC 3339, UTC. Null if never scanned.","type":["string","null"]},"lei":{"description":"Legal Entity Identifier (ISO 17442), for DORA RoI.","type":["string","null"]},"hq_country":{"description":"Head-office country, ISO 3166-1 alpha-2.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","name","legal_name","domain","category","vendor_type","access_level","business_criticality","data_sensitivity","economic_impact","active","monitoring_status","last_scan_at","lei","hq_country","created_at","updated_at"],"additionalProperties":false,"description":"A third party in the tenant inventory."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/vendors/{id}":{"get":{"operationId":"getVendor","summary":"Fetch one third party","description":"The same object `listVendors` returns, for a single vendor.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["vendors"],"parameters":[{"name":"id","in":"path","required":true,"description":"Vendor identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Stable identifier of the vendor in this tenant."},"name":{"type":"string","description":"Display name."},"legal_name":{"description":"Registered legal name, when it differs.","type":["string","null"]},"domain":{"description":"Primary domain, lowercase, no scheme.","type":["string","null"]},"category":{"description":"Free-text category assigned by the tenant. NOT a closed vocabulary: the console form stores the label of the tenant’s language, and the API accepts any text. List the vendors to see the values a tenant uses.","type":["string","null"]},"vendor_type":{"description":"Kind of third party: software | cloud | hardware | service_provider. Open vocabulary: a new kind is a compatible change.","x-hs-known-values":["software","cloud","hardware","service_provider"],"type":["string","null"]},"access_level":{"description":"Level of access the vendor has to the tenant systems.","type":["string","null"]},"business_criticality":{"description":"low | medium | high | critical, as set by the tenant.","type":["string","null"]},"data_sensitivity":{"description":"public | internal | confidential | restricted.","type":["string","null"]},"economic_impact":{"description":"Estimated economic impact of an outage, in the tenant currency.","type":["number","null"]},"active":{"type":"boolean","description":"Whether the vendor is operationally enabled."},"monitoring_status":{"description":"Continuous-monitoring state.","type":["string","null"]},"last_scan_at":{"description":"RFC 3339, UTC. Null if never scanned.","type":["string","null"]},"lei":{"description":"Legal Entity Identifier (ISO 17442), for DORA RoI.","type":["string","null"]},"hq_country":{"description":"Head-office country, ISO 3166-1 alpha-2.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","name","legal_name","domain","category","vendor_type","access_level","business_criticality","data_sensitivity","economic_impact","active","monitoring_status","last_scan_at","lei","hq_country","created_at","updated_at"],"additionalProperties":false,"description":"A third party in the tenant inventory."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/alerts":{"get":{"operationId":"listAlerts","summary":"List supply-chain alerts","description":"Alerts raised against the tenant vendors. Poll incrementally with `since` plus `order=asc`.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["alerts"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at.","schema":{"default":"created_at","type":"string","enum":["created_at"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"status","in":"query","required":false,"description":"Filter by the derived status.","schema":{"type":"string","enum":["open","acknowledged","resolved"]}},{"name":"severity","in":"query","required":false,"description":"Exact severity match, in the vocabulary the response uses (critical | high | medium | low | info). The legacy Spanish values (critico, alto, medio, bajo, info) are still accepted and mean the same; they are documented as accepted until 2027-01-14, so move to the English ones.","schema":{"x-hs-known-values":["critical","high","medium","low","info"],"x-hs-deprecated-values":["critico","alto","medio","bajo"],"x-hs-deprecated-values-sunset":"2027-01-14","type":"string","minLength":1,"maxLength":40}},{"name":"vendor_id","in":"query","required":false,"description":"Only alerts about this vendor.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"since","in":"query","required":false,"description":"Only alerts created at or after this RFC 3339 instant.","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"vendor_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The vendor this alert is about (`/v1/vendors/{id}`)."},"type":{"type":"string","description":"Alert family, e.g. `breach`, `cve`, `rating_drop`."},"severity":{"description":"critical | high | medium | low | info, when classified. Open vocabulary: a new value is a compatible change.","x-hs-known-values":["critical","high","medium","low","info"],"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"source_url":{"description":"Where the finding came from, when public.","type":["string","null"]},"economic_impact":{"type":["number","null"]},"status":{"type":"string","enum":["open","acknowledged","resolved"],"description":"Derived: resolved if closed, acknowledged if taken, otherwise open."},"acknowledged_at":{"description":"RFC 3339, UTC.","type":["string","null"]},"resolved_at":{"description":"RFC 3339, UTC.","type":["string","null"]},"resolution_reason":{"description":"Why it was closed, when it was.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","vendor_id","type","severity","title","description","source_url","economic_impact","status","acknowledged_at","resolved_at","resolution_reason","created_at"],"additionalProperties":false,"description":"A supply-chain alert raised against one vendor."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/alerts/{id}":{"get":{"operationId":"getAlert","summary":"Fetch one alert","description":"The same object `listAlerts` returns, for a single alert.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["alerts"],"parameters":[{"name":"id","in":"path","required":true,"description":"Alert identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"vendor_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The vendor this alert is about (`/v1/vendors/{id}`)."},"type":{"type":"string","description":"Alert family, e.g. `breach`, `cve`, `rating_drop`."},"severity":{"description":"critical | high | medium | low | info, when classified. Open vocabulary: a new value is a compatible change.","x-hs-known-values":["critical","high","medium","low","info"],"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"source_url":{"description":"Where the finding came from, when public.","type":["string","null"]},"economic_impact":{"type":["number","null"]},"status":{"type":"string","enum":["open","acknowledged","resolved"],"description":"Derived: resolved if closed, acknowledged if taken, otherwise open."},"acknowledged_at":{"description":"RFC 3339, UTC.","type":["string","null"]},"resolved_at":{"description":"RFC 3339, UTC.","type":["string","null"]},"resolution_reason":{"description":"Why it was closed, when it was.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","vendor_id","type","severity","title","description","source_url","economic_impact","status","acknowledged_at","resolved_at","resolution_reason","created_at"],"additionalProperties":false,"description":"A supply-chain alert raised against one vendor."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/risks":{"get":{"operationId":"listRisks","summary":"List risk-register entries","description":"The tenant risk register, with inherent and residual figures.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["risks"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, updated_at, inherent_score, residual_score.","schema":{"default":"created_at","type":"string","enum":["created_at","updated_at","inherent_score","residual_score"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"status","in":"query","required":false,"description":"Exact status match. Known values: identified, assessed, treatment_planned, treatment_implemented, closed. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["identified","assessed","treatment_planned","treatment_implemented","closed"],"type":"string","minLength":1,"maxLength":24}},{"name":"domain","in":"query","required":false,"description":"Exact domain match. Known values: vendor, cyber, data, operational, compliance, concentration, strategic, legal, financial, reputational. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["vendor","cyber","data","operational","compliance","concentration","strategic","legal","financial","reputational"],"type":"string","minLength":1,"maxLength":20}},{"name":"vendor_id","in":"query","required":false,"description":"Only risks linked to this vendor.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"min_inherent_score","in":"query","required":false,"description":"Only risks whose inherent score is at least this.","schema":{"type":"integer","minimum":0,"maximum":9007199254740991}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"title":{"type":"string"},"description":{"type":["string","null"]},"domain":{"type":"string","description":"Risk domain: vendor | cyber | data | operational | compliance | concentration | strategic | legal | financial | reputational.","x-hs-known-values":["vendor","cyber","data","operational","compliance","concentration","strategic","legal","financial","reputational"]},"status":{"type":"string","description":"Risk lifecycle status: identified | assessed | treatment_planned | treatment_implemented | closed.","x-hs-known-values":["identified","assessed","treatment_planned","treatment_implemented","closed"]},"treatment":{"description":"mitigate | accept | transfer | avoid.","type":["string","null"]},"treatment_notes":{"type":["string","null"]},"threat_source":{"type":["string","null"]},"vulnerability":{"type":["string","null"]},"likelihood_inherent":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"impact_inherent":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"inherent_score":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"likelihood_residual":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"impact_residual":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"residual_score":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"source":{"type":"string","description":"manual | imported | derived."},"owner_user_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"User of this tenant who owns the risk."},"vendor_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The vendor this risk is about, when any."},"control_codes":{"type":"array","items":{"type":"string"},"description":"Framework control codes linked to the risk."},"review_date":{"description":"Calendar date, `YYYY-MM-DD`.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","title","description","domain","status","treatment","treatment_notes","threat_source","vulnerability","likelihood_inherent","impact_inherent","inherent_score","likelihood_residual","impact_residual","residual_score","source","owner_user_id","vendor_id","control_codes","review_date","created_at","updated_at"],"additionalProperties":false,"description":"An entry of the tenant risk register."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/risks/{id}":{"get":{"operationId":"getRisk","summary":"Fetch one risk-register entry","description":"The same object `listRisks` returns, for a single entry.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["risks"],"parameters":[{"name":"id","in":"path","required":true,"description":"Risk identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"title":{"type":"string"},"description":{"type":["string","null"]},"domain":{"type":"string","description":"Risk domain: vendor | cyber | data | operational | compliance | concentration | strategic | legal | financial | reputational.","x-hs-known-values":["vendor","cyber","data","operational","compliance","concentration","strategic","legal","financial","reputational"]},"status":{"type":"string","description":"Risk lifecycle status: identified | assessed | treatment_planned | treatment_implemented | closed.","x-hs-known-values":["identified","assessed","treatment_planned","treatment_implemented","closed"]},"treatment":{"description":"mitigate | accept | transfer | avoid.","type":["string","null"]},"treatment_notes":{"type":["string","null"]},"threat_source":{"type":["string","null"]},"vulnerability":{"type":["string","null"]},"likelihood_inherent":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"impact_inherent":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"inherent_score":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"likelihood_residual":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"impact_residual":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"residual_score":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"source":{"type":"string","description":"manual | imported | derived."},"owner_user_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"User of this tenant who owns the risk."},"vendor_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The vendor this risk is about, when any."},"control_codes":{"type":"array","items":{"type":"string"},"description":"Framework control codes linked to the risk."},"review_date":{"description":"Calendar date, `YYYY-MM-DD`.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","title","description","domain","status","treatment","treatment_notes","threat_source","vulnerability","likelihood_inherent","impact_inherent","inherent_score","likelihood_residual","impact_residual","residual_score","source","owner_user_id","vendor_id","control_codes","review_date","created_at","updated_at"],"additionalProperties":false,"description":"An entry of the tenant risk register."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/compliance/controls":{"get":{"operationId":"listComplianceControls","summary":"List control status for a framework","description":"Per-control status of one framework. Requires the read scope OF THAT FRAMEWORK: `compliance.:read`.\n\nRequired scope: `compliance.:read`. Published in `/v1` since 2026-09-20.","tags":["compliance"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: control_code, last_evaluated_at.","schema":{"default":"control_code","type":"string","enum":["control_code","last_evaluated_at"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"framework","in":"query","required":true,"description":"Framework slug. One of: nis2, ens, iso27001, dora, gdpr, soc2, iso42001, nistcsf, pcidss, euaiact, hipaa, cmmc, cyberessentials, uaeia, sama, saudiecc, ndpa.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Exact status match. Known values: covered, partial, missing. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["covered","partial","missing"],"type":"string","minLength":1,"maxLength":20}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"framework":{"type":"string","description":"Framework slug the control belongs to."},"control_code":{"type":"string","description":"Control identifier inside the framework."},"status":{"type":"string","description":"covered | partial | missing, as evaluated by the platform.","x-hs-known-values":["covered","partial","missing"]},"evidence_count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Evidence items currently linked to the control."},"last_evaluated_at":{"description":"RFC 3339, UTC.","type":["string","null"]}},"required":["framework","control_code","status","evidence_count","last_evaluated_at"],"additionalProperties":false,"description":"The tenant status of one framework control."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":[]},{"portfolioKey":[]}],"x-hs-scope":{"template":"compliance.:read"}}},"/v1/compliance/frameworks/{framework}":{"get":{"operationId":"getFrameworkPosture","summary":"Framework posture summary","description":"Counts of evaluated controls by status for one framework, for a dashboard tile. Requires `compliance.:read`.\n\nRequired scope: `compliance.:read`. Published in `/v1` since 2026-09-20.","tags":["compliance"],"parameters":[{"name":"framework","in":"path","required":true,"description":"Framework slug. One of: nis2, ens, iso27001, dora, gdpr, soc2, iso42001, nistcsf, pcidss, euaiact, hipaa, cmmc, cyberessentials, uaeia, sama, saudiecc, ndpa.","schema":{"type":"string"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"framework":{"type":"string"},"controls_total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Controls the platform has evaluated."},"by_status":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":"Control count per status. Keys are whatever statuses exist; may grow."},"evidence_total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Sum of the evidence linked across the evaluated controls."},"last_evaluated_at":{"description":"Most recent control evaluation, RFC 3339 UTC. Null if never evaluated.","type":["string","null"]}},"required":["framework","controls_total","by_status","evidence_total","last_evaluated_at"],"additionalProperties":false,"description":"Posture summary for one framework."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":[]},{"portfolioKey":[]}],"x-hs-scope":{"template":"compliance.:read"}}},"/v1/evidence":{"get":{"operationId":"listEvidence","summary":"List security evidence","description":"Evidence held for the tenant. Use `expiring_before` plus `sort=valid_until&order=asc` for a renewal queue.\n\nRequired scope: `evidence:read`. Published in `/v1` since 2026-09-20.","tags":["evidence"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, valid_until.","schema":{"default":"created_at","type":"string","enum":["created_at","valid_until"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"evidence_type","in":"query","required":false,"description":"Exact type match.","schema":{"type":"string","minLength":1,"maxLength":50}},{"name":"source_type","in":"query","required":false,"description":"Exact producer match.","schema":{"type":"string","minLength":1,"maxLength":50}},{"name":"vendor_id","in":"query","required":false,"description":"Only evidence about this vendor.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"expiring_before","in":"query","required":false,"description":"Only evidence with a `valid_until` strictly before this `YYYY-MM-DD`.","schema":{"type":"string","format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"evidence_type":{"type":"string","description":"What kind of evidence this is."},"source_type":{"type":"string","description":"Which subsystem produced it."},"source_id":{"type":"string","description":"Its identifier inside that subsystem."},"title":{"type":"string"},"description":{"type":["string","null"]},"vendor_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The vendor it is about, when any."},"asset_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The asset it is about, when any."},"auto_generated":{"type":"boolean","description":"False when a person uploaded it."},"valid_until":{"description":"Calendar date, `YYYY-MM-DD`. Null = no expiry.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","evidence_type","source_type","source_id","title","description","vendor_id","asset_id","auto_generated","valid_until","created_at"],"additionalProperties":false,"description":"One item of security evidence."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["evidence:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["evidence:read"]}}},"/v1/evidence/{id}":{"get":{"operationId":"getEvidence","summary":"Fetch one evidence item","description":"The same object `listEvidence` returns, for a single item.\n\nRequired scope: `evidence:read`. Published in `/v1` since 2026-09-20.","tags":["evidence"],"parameters":[{"name":"id","in":"path","required":true,"description":"Evidence identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"evidence_type":{"type":"string","description":"What kind of evidence this is."},"source_type":{"type":"string","description":"Which subsystem produced it."},"source_id":{"type":"string","description":"Its identifier inside that subsystem."},"title":{"type":"string"},"description":{"type":["string","null"]},"vendor_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The vendor it is about, when any."},"asset_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The asset it is about, when any."},"auto_generated":{"type":"boolean","description":"False when a person uploaded it."},"valid_until":{"description":"Calendar date, `YYYY-MM-DD`. Null = no expiry.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","evidence_type","source_type","source_id","title","description","vendor_id","asset_id","auto_generated","valid_until","created_at"],"additionalProperties":false,"description":"One item of security evidence."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["evidence:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["evidence:read"]}}},"/v1/endpoints":{"get":{"operationId":"listEndpoints","summary":"List enrolled endpoint agents","description":"The endpoint agents enrolled in this tenant. `silent_since` selects the ones that have gone quiet, including those that never reported.\n\nRequired scope: `endpoints:read`. Published in `/v1` since 2026-09-20.","tags":["endpoints"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: enrolled_at, last_seen_at, producer.","schema":{"default":"enrolled_at","type":"string","enum":["enrolled_at","last_seen_at","producer"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"status","in":"query","required":false,"description":"Exact status match.","schema":{"type":"string","enum":["active","revoked"]}},{"name":"silent_since","in":"query","required":false,"description":"Agents last seen before this RFC 3339 instant, or never seen at all.","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"producer":{"type":"string","description":"The agent name, unique among the active ones of the tenant."},"producer_kind":{"type":"string","description":"What kind of producer it is."},"status":{"type":"string","description":"active | revoked."},"signature_alg":{"type":"string","description":"Algorithm this enrollment signs with."},"agent_version":{"type":["string","null"]},"hostname":{"description":"Last hostname the agent reported.","type":["string","null"]},"enrolled_at":{"type":"string","description":"RFC 3339, UTC."},"last_seen_at":{"description":"RFC 3339, UTC. Null until the first envelope.","type":["string","null"]},"revoked_at":{"description":"RFC 3339, UTC.","type":["string","null"]}},"required":["id","producer","producer_kind","status","signature_alg","agent_version","hostname","enrolled_at","last_seen_at","revoked_at"],"additionalProperties":false,"description":"An endpoint agent enrolled in this tenant."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["endpoints:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["endpoints:read"]}}},"/v1/portfolio/tenants":{"get":{"operationId":"listPortfolioTenants","summary":"List the client tenants of an MSSP portfolio","description":"Every tenant the presented PORTFOLIO key may select with `X-HS-Tenant`: the MSSP’s own tenants (its internal one included) and every tenant of its active client organisations, active or not, licensed or not, narrowed to the key’s client subset when it has one. Membership is evaluated live on every request. Only a portfolio key (`hsp_…`) can call this operation; this operation never takes `X-HS-Tenant`.\n\nRequired scope: `portfolio:read`. Published in `/v1` since 2026-10-16.","tags":["Portfolio"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: name, slug.","schema":{"default":"name","type":"string","enum":["name","slug"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Stable identifier (lowercase). The ONLY value `X-HS-Tenant` accepts."},"name":{"type":"string","description":"Display name. Untrusted text."},"slug":{"type":"string","description":"Short name, unique across the platform. A client may resolve a slug to an `id` from this list; the server never accepts a slug as a selector."},"organization":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"description":"The organisation the tenant belongs to: the MSSP itself for the tenants it holds directly (including its internal one), or the client organisation."},"internal":{"type":"boolean","description":"True only for the MSSP’s own internal tenant."},"active":{"type":"boolean","description":"Whether the tenant is enabled. A deactivated tenant is listed but cannot be read."},"licence_status":{"type":"string","description":"Open vocabulary: `in_force` (licence in force), `grace` (grace period: reads are served), `inactive` (none, suspended, revoked or expired: data operations answer 403 `LICENCE_INACTIVE`). Treat an unknown value as \"may not be readable\". A tenant is readable when `active` is true and `licence_status` is not `inactive`.","x-hs-known-values":["in_force","grace","inactive"]}},"required":["id","name","slug","organization","internal","active","licence_status"],"additionalProperties":false,"description":"A client tenant an MSSP portfolio key may select."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"portfolioKey":[]}],"x-hs-scope":{"scopes":["portfolio:read"]}}},"/v1/me":{"get":{"operationId":"whoAmI","summary":"Describe the calling credential","description":"Who the presented credential is: its type and scopes, and the tenant and organisation it reads (a tenant API key or an OAuth token), or the MSSP organisation it belongs to and how many tenants it can reach (a portfolio key; the tenants themselves are on `GET /v1/portfolio/tenants`). It describes the caller only: no other tenant, no secret, never the key or its hash. Any valid credential may call it: it requires NO scope, and it uses the same authentication and the same rate limits as every other operation. It never takes `X-HS-Tenant`.\n\nRequired scope: `none (any valid credential)`. Published in `/v1` since 2026-10-17.","tags":["Identity"],"parameters":[],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"credential":{"type":"object","properties":{"type":{"type":"string","description":"Open vocabulary: `tenant` (a tenant API key, `hsk_…`), `oauth` (a user access token, `hs_at_…`) or `portfolio` (an MSSP portfolio key, `hsp_…`).","x-hs-known-values":["tenant","oauth","portfolio"]},"prefix":{"description":"The clear key prefix (`hsp_…`) of a portfolio key. Null for a tenant key or an OAuth token. Never the full key and never a hash.","type":["string","null"]},"scopes":{"type":"array","items":{"type":"string"},"description":"The scopes the credential carries."}},"required":["type","prefix","scopes"],"additionalProperties":false,"description":"The credential that made this call."},"tenant":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The tenant this credential reads (lowercase)."},"name":{"type":"string","description":"Display name. Untrusted text."},"type":{"type":"string","description":"The type of the organisation that owns the tenant. Open vocabulary: `direct` (a customer organisation), `mssp` (a managed security service provider) or `root` (the platform operator). Treat an unknown value as opaque.","x-hs-known-values":["direct","mssp","root"]}},"required":["id","name","type"],"additionalProperties":false},{"type":"null"}],"description":"The credential’s own tenant. Null for a portfolio key, which has no tenant."},"organization":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"description":"Display name. Null for a portfolio key whose client subset holds none of the provider’s own tenants, the only place a portfolio key sees the name. Untrusted text.","type":["string","null"]},"type":{"type":"string","description":"`direct`, `mssp` or `root`; `mssp` for a portfolio key."}},"required":["id","name","type"],"additionalProperties":false,"description":"The organisation of the tenant (tenant key, OAuth token) or the MSSP the key belongs to (portfolio key)."},"portfolio":{"anyOf":[{"type":"object","properties":{"tenant_count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many tenants this key can select, right now."},"tenants_path":{"type":"string","description":"Where the tenants are listed: `GET /v1/portfolio/tenants`."}},"required":["tenant_count","tenants_path"],"additionalProperties":false},{"type":"null"}],"description":"Portfolio keys only. Null for a tenant key or an OAuth token."}},"required":["credential","tenant","organization","portfolio"],"additionalProperties":false,"description":"The credential that made the call, and what it may see about itself."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":[]},{"portfolioKey":[]}],"x-hs-scope":{"none":true}}},"/v1/reports":{"get":{"operationId":"listReports","summary":"List generated reports","description":"The reports generated for the tenant, newest first by default. Poll for new ones with `since` plus `order=asc`; download one with `downloadReport` once its `status` is `completed`.\n\nRequired scope: `reports:read`. Published in `/v1` since 2026-10-19.","tags":["reports"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, period_end.","schema":{"default":"created_at","type":"string","enum":["created_at","period_end"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"report_type","in":"query","required":false,"description":"Exact type match. Known values: monthly, vendor_risk, nis2_compliance, ens, cyberbrief, compliance, security_posture, board_report. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["monthly","vendor_risk","nis2_compliance","ens","cyberbrief","compliance","security_posture","board_report"],"type":"string","minLength":1,"maxLength":50}},{"name":"status","in":"query","required":false,"description":"Exact status match. Known values: generating, completed, failed. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["generating","completed","failed"],"type":"string","minLength":1,"maxLength":20}},{"name":"since","in":"query","required":false,"description":"Only reports created at or after this RFC 3339 instant.","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"report_type":{"type":"string","description":"What kind of report this is. Known values: monthly, vendor_risk, nis2_compliance, ens, cyberbrief, compliance, security_posture, board_report. Open vocabulary: a new kind may appear.","x-hs-known-values":["monthly","vendor_risk","nis2_compliance","ens","cyberbrief","compliance","security_posture","board_report"]},"title":{"type":"string"},"period_start":{"type":"string","description":"First day the report covers, `YYYY-MM-DD`."},"period_end":{"type":"string","description":"Last day the report covers, `YYYY-MM-DD`."},"status":{"type":"string","description":"Lifecycle: generating | completed | failed. Only a `completed` report can be downloaded.","x-hs-known-values":["generating","completed","failed"]},"created_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","report_type","title","period_start","period_end","status","created_at"],"additionalProperties":false,"description":"A report the platform generated for the tenant (metadata only)."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["reports:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["reports:read"]}}},"/v1/reports/{id}":{"get":{"operationId":"getReport","summary":"Fetch one report","description":"The same object `listReports` returns, for a single report, plus the formats it can be downloaded in now (`download_formats`).\n\nRequired scope: `reports:read`. Published in `/v1` since 2026-10-19.","tags":["reports"],"parameters":[{"name":"id","in":"path","required":true,"description":"Report identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"report_type":{"type":"string","description":"What kind of report this is. Known values: monthly, vendor_risk, nis2_compliance, ens, cyberbrief, compliance, security_posture, board_report. Open vocabulary: a new kind may appear.","x-hs-known-values":["monthly","vendor_risk","nis2_compliance","ens","cyberbrief","compliance","security_posture","board_report"]},"title":{"type":"string"},"period_start":{"type":"string","description":"First day the report covers, `YYYY-MM-DD`."},"period_end":{"type":"string","description":"Last day the report covers, `YYYY-MM-DD`."},"status":{"type":"string","description":"Lifecycle: generating | completed | failed. Only a `completed` report can be downloaded.","x-hs-known-values":["generating","completed","failed"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"download_formats":{"type":"array","items":{"type":"string","description":"A format `downloadReport` serves. Known values: pdf, xlsx. Open vocabulary: a format may be added.","x-hs-known-values":["pdf","xlsx"]},"description":"The formats `downloadReport` can serve for this report now. Empty while it is not `completed`, or when its content has no renderer (download it from the console as Markdown or JSON)."}},"required":["id","report_type","title","period_start","period_end","status","created_at","download_formats"],"additionalProperties":false,"description":"One report, with the formats it can be downloaded in."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["reports:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["reports:read"]}}},"/v1/reports/{id}/download":{"get":{"operationId":"downloadReport","summary":"Download a report file","description":"The report rendered as a file (PDF or XLSX), returned as the response body with `Content-Disposition: attachment`; never a link. Each delivery is recorded in the tenant’s audit trail with the credential that received it. A report that is not `completed`, a format its content has no renderer for (see `download_formats` in `getReport`) and a file over the size cap are a 409.\n\nRequired scope: `reports:read`. Published in `/v1` since 2026-10-19.","tags":["reports"],"parameters":[{"name":"id","in":"path","required":true,"description":"Report identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"format","in":"query","required":false,"description":"File format, one of: pdf, xlsx. Defaults to pdf. `getReport` lists the formats a report can be served in (`download_formats`).","schema":{"default":"pdf","type":"string","enum":["pdf","xlsx"]}},{"name":"locale","in":"query","required":false,"description":"Language of the file, one of: es, en, de, fr, it, eu. Defaults to en.","schema":{"default":"en","type":"string","enum":["es","en","de","fr","it","eu"]}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The file, as an attachment. At most 20971520 bytes; a larger one is a 409 `FILE_TOO_LARGE`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}},"Content-Disposition":{"description":"`attachment; filename=\".\"`. The name is `[A-Za-z0-9._-]`.","schema":{"type":"string"}},"Content-Length":{"description":"Size of the file in bytes.","schema":{"type":"integer","minimum":0}}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The file cannot be served as asked. `error.details.code` says why: `REPORT_NOT_READY` (the report is still `generating`, or `failed`; `error.details.status` says which. Retry a `generating` one later); `FORMAT_UNSUPPORTED` (the report’s content has no renderer for that format; `error.details.formats` lists the ones it has, possibly none); `FILE_TOO_LARGE` (the file is larger than 20971520 bytes; `error.details.limit_bytes` says the cap, and no byte is sent).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["reports:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["reports:read"]}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"additionalProperties":false,"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","enum":["invalid_request","unauthorized","forbidden","not_found","method_not_allowed","conflict","rate_limited","internal_error","service_unavailable"],"description":"Closed vocabulary; new members may be added (see Versioning)."},"message":{"type":"string","description":"For a human reading a log. Not stable; do not parse it."},"request_id":{"type":"string","description":"Also the `X-Request-Id` response header."},"details":{"type":"object","additionalProperties":true,"description":"Diagnostic, except `code` and the keys the operation’s error descriptions name, which are contract.","properties":{"code":{"type":"string","description":"OPEN vocabulary (a value keeps its meaning and its HTTP status; new values may be added): `TENANT_HEADER_INVALID`, `TENANT_REQUIRED`, `TENANT_SELECTOR_NOT_SUPPORTED` (400), `INSUFFICIENT_SCOPE`, `LICENCE_INACTIVE` (403), `TENANT_NOT_FOUND` (404), `REPORT_NOT_READY`, `FORMAT_UNSUPPORTED`, `FILE_TOO_LARGE` (409).","x-hs-known-values":["TENANT_HEADER_INVALID","TENANT_REQUIRED","TENANT_SELECTOR_NOT_SUPPORTED","INSUFFICIENT_SCOPE","LICENCE_INACTIVE","TENANT_NOT_FOUND","REPORT_NOT_READY","FORMAT_UNSUPPORTED","FILE_TOO_LARGE"]}}}}}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A tenant API key, created in the console under Settings → API keys with the minimum scopes it needs."},"portfolioKey":{"type":"http","scheme":"bearer","description":"An MSSP portfolio key (`hsp_…`), created by an organisation administrator of an active managed security service provider in the console’s MSSP area (API keys). It lists the provider’s client tenants (`portfolio:read`) and reads ONE of them per request, selected with `X-HS-Tenant`, with the same data scopes a tenant API key uses. Read-only; mandatory expiry (default 90 days, at most 365); optional client subset and source IP ranges. Refused with 401 everywhere but `/v1`."},"oauth2":{"type":"oauth2","description":"A USER access token (`hs_at_…`) issued by the HodeiShield console, the OAuth 2.1 authorization server at `https://app.hodeishield.com/api/auth` (metadata: `/.well-known/oauth-authorization-server` on its origin). Authorization code with PKCE (S256, mandatory) for applications with a browser; the RFC 8628 device authorization grant (`https://app.hodeishield.com/api/auth/device/code`) for those without one. Every token is bound to ONE user and ONE tenant, chosen by the user on the consent screen, and carries read scopes only; the tenant is re-checked against what the user can see on every request. Public clients register themselves (RFC 7591) at `/oauth2/register`; the HodeiShield CLI is pre-registered as `hodeishield-cli`. Revocation: RFC 7009 at `/oauth2/revoke`, or by the user in the console under Account → Application access.","flows":{"authorizationCode":{"authorizationUrl":"https://app.hodeishield.com/api/auth/oauth2/authorize","tokenUrl":"https://app.hodeishield.com/api/auth/oauth2/token","refreshUrl":"https://app.hodeishield.com/api/auth/oauth2/token","scopes":{"supply_risk:read":"Read supply_risk data of the tenant the token is bound to.","compliance.nis2:read":"Read compliance.nis2 data of the tenant the token is bound to.","compliance.ens:read":"Read compliance.ens data of the tenant the token is bound to.","compliance.iso27001:read":"Read compliance.iso27001 data of the tenant the token is bound to.","compliance.dora:read":"Read compliance.dora data of the tenant the token is bound to.","compliance.gdpr:read":"Read compliance.gdpr data of the tenant the token is bound to.","compliance.soc2:read":"Read compliance.soc2 data of the tenant the token is bound to.","compliance.iso42001:read":"Read compliance.iso42001 data of the tenant the token is bound to.","compliance.nistcsf:read":"Read compliance.nistcsf data of the tenant the token is bound to.","compliance.pcidss:read":"Read compliance.pcidss data of the tenant the token is bound to.","compliance.euaiact:read":"Read compliance.euaiact data of the tenant the token is bound to.","compliance.hipaa:read":"Read compliance.hipaa data of the tenant the token is bound to.","compliance.cmmc:read":"Read compliance.cmmc data of the tenant the token is bound to.","compliance.cyberessentials:read":"Read compliance.cyberessentials data of the tenant the token is bound to.","compliance.uaeia:read":"Read compliance.uaeia data of the tenant the token is bound to.","compliance.sama:read":"Read compliance.sama data of the tenant the token is bound to.","compliance.saudiecc:read":"Read compliance.saudiecc data of the tenant the token is bound to.","compliance.ndpa:read":"Read compliance.ndpa data of the tenant the token is bound to.","radar:read":"Read radar data of the tenant the token is bound to.","connectors:read":"Read connectors data of the tenant the token is bound to.","endpoints:read":"Read endpoints data of the tenant the token is bound to.","hodeiai:read":"Read hodeiai data of the tenant the token is bound to.","virtual_ciso:read":"Read virtual_ciso data of the tenant the token is bound to.","simulator:read":"Read simulator data of the tenant the token is bound to.","evidence:read":"Read evidence data of the tenant the token is bound to.","reports:read":"Read reports data of the tenant the token is bound to.","ai_governance:read":"Read ai_governance data of the tenant the token is bound to.","management:read":"Read management data of the tenant the token is bound to.","offline_access":"Issue a refresh token (30 days, rotated on every use)."}}}}}},"security":[{"bearerAuth":[]},{"oauth2":[]},{"portfolioKey":[]}]} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Hodei-Shield API","version":"2026-10-25","summary":"Third-party risk and compliance posture, for your own systems.","description":"## Versioning\n\n`/v1` is frozen. These changes are **compatible** and will happen inside it\nwithout notice:\n\n- a new operation;\n- a new **optional** query parameter;\n- a new field in a response object;\n- a new value in a field documented as an open vocabulary (`status`,\n `severity`, `evidence_type`, the keys of `by_status`);\n- a new `error.code`, always with the HTTP status its siblings already use;\n- anything under `error.details`, which is diagnostic and not part of the\n contract — except `error.details.code` and the keys the error table of\n the operation names (`missing`, `licence`, `parameter`, `scope`,\n `limit`, `window_seconds`, `retry_after_seconds`, `status`,\n `formats`, `limit_bytes`): those are contract.\n `error.details.code` is an OPEN vocabulary: a value, once published, keeps\n its meaning and the HTTP status it is sent with; new values may be added.\n\nWrite your client so those do not break it: ignore unknown fields, do not\nswitch exhaustively on a documented-open vocabulary, and treat an unknown\n`error.code` as \"an error of this HTTP status\".\n\nThese changes are **incompatible** and will NOT happen inside `/v1`. They go\nto `/v2`, and `/v1` keeps serving:\n\n- removing or renaming an operation, a parameter or a field;\n- changing the type, the unit or the meaning of a field;\n- making an optional parameter required, or narrowing what one accepts;\n- changing which scope an operation requires;\n- changing the HTTP status an existing `error.code` is sent with.\n\n## Read-only\n\n`/v1` only reads. Every operation is a `GET`. Any other method (`POST`, `PUT`,\n`PATCH`, `DELETE`) on a path that exists answers **405** with `Allow: GET`, and on\na path that does not exist answers **404**: neither changes any state. A write\nwill be offered only as a new operation, which is a compatible change, once a\ncustomer asks for a concrete one.\n\n## Downloads\n\nAn operation whose success is a FILE (`GET /v1/reports/{id}/download`) answers\n**200** with the bytes, the `Content-Type` of the format asked for and\n`Content-Disposition: attachment; filename=\"…\"`; it never answers a URL to fetch\nthem from. Its errors are the same JSON envelope as everywhere else. A file that\ncannot be served as asked is a **409** with `error.details.code`: the report is\nnot ready yet (`REPORT_NOT_READY`), its content has no renderer for that format\n(`FORMAT_UNSUPPORTED`) or the file exceeds the size the operation serves\n(`FILE_TOO_LARGE`, with `limit_bytes`). Every file delivered is recorded in the\ntenant's audit trail with the credential that received it.\n\n## Authentication\n\nEvery operation takes `Authorization: Bearer `. Three credentials\nexist:\n\n- a **tenant API key** (`hsk_…`): belongs to ONE tenant and carries an\n explicit list of scopes; it can read nothing outside that tenant, whatever it\n asks for;\n- a user's **OAuth access token** (`hs_at_…`): one user, ONE tenant chosen\n at consent, read scopes only;\n- an MSSP **portfolio key** (`hsp_…`): held by a managed security service\n provider, it reads ANY of its client tenants, ONE per request, selected with\n the request header `X-HS-Tenant: ` (a UUID, never a slug).\n `GET /v1/portfolio/tenants` lists the tenants it may select. Read-only, with\n a mandatory expiry. A tenant outside the key's portfolio answers **404**\n (`error.details.code: \"TENANT_NOT_FOUND\"`), exactly as one that does not\n exist.\n\nEvery successful (2xx) answer of a data operation carries `X-HS-Tenant` with\nthe id of the tenant whose data the body holds, for every credential; no error\ncarries it. A tenant API key or OAuth token may send `X-HS-Tenant` only with\nits own tenant's id; any other value is a **400**\n(`TENANT_SELECTOR_NOT_SUPPORTED`).\n\nEach operation below declares the scope it requires. A credential that\nauthenticates but lacks the scope is answered **403**\n(`error.details.code: \"INSUFFICIENT_SCOPE\"`), never 404: you are told what you\nare missing, not that the resource does not exist.\n\n## Errors\n\nEvery error has the same body:\n\n```json\n{ \"error\": { \"code\": \"forbidden\", \"message\": \"…\", \"request_id\": \"…\" } }\n```\n\nBranch on `code`, log `request_id`, show `message` to nobody but yourself.\n`request_id` is also the `X-Request-Id` response header, and it is what\nsupport will ask you for.\n\n## Rate limits\n\nEvery response that got past authentication carries the limit that is closest\nto being exhausted:\n\n- `RateLimit-Limit` — requests allowed in the window;\n- `RateLimit-Remaining` — requests left in the current window;\n- `RateLimit-Reset` — **seconds** until the window renews;\n- `RateLimit-Policy` — `;w=`.\n\nThree limits apply, each per one-minute window: per API key, per organisation\n(all of its keys together) and per service. The first two come from the licence\nof the organisation the key belongs to: a higher licence tier has more room, and\nan MSSP partner has more than a direct customer of the same tier. Read the\ncurrent limit from `RateLimit-Limit` and `RateLimit-Policy` rather than\nhard-coding it. When one is exhausted the answer is\n**429** with `error.code: \"rate_limited\"`, `Retry-After` (seconds) and\n`error.details.scope` (`key`, `tenant` or `service`) saying which. A\nportfolio key is limited per key (`portfolio_key`), per provider across all\nits portfolio keys (`portfolio_org`) and per service; once the selected tenant\nis known to be in its portfolio, the client tenant's own limit (`tenant`) and\nthe provider's share of it (`portfolio_tenant`) apply too. Your\norganisation's usage never counts against another's. Windows are fixed, so a\nclient can burst up to twice the limit across a window boundary: back off on\n`Retry-After` rather than pacing to the second. The limits are part of the\nservice, not of the frozen contract: they may be raised without notice.\n\n## Pagination, filtering and ordering\n\nEvery collection takes `page` and `per_page` (max 200) and answers with a\n`pagination` object carrying `total` and `total_pages`. Every collection\ntakes `sort` (an enum of the fields THAT collection can order by) and\n`order` (`asc`/`desc`, default `desc`). Filters are exact matches unless\nthe parameter says otherwise. A parameter or field with a known vocabulary lists\nit in `x-hs-known-values` (an OpenAPI extension, not a JSON Schema `enum`: the\nserver still accepts what it accepted before). Values retired from a parameter\nare listed in `x-hs-deprecated-values`, with the date until which the server is\ndocumented to accept them in `x-hs-deprecated-values-sunset`.\n\n## Contract changes\n\nCorrections and additions to the published contract, newest first. They ship\nwith the contract version (`info.version`) named in each entry. Detect a\ncapability by the operation, not by this string: the portfolio contract is\npresent when `GET /v1` lists `listPortfolioTenants`.\n\n- **2026-10-24 — correction: `beta` on `GET /v1/me` is the tenant's own.** For\n a client tenant of an MSSP whose own organisation is not in a running beta, `beta`\n is `active`, `running: true` and `ends_at: null` while the MSSP's beta runs\n (it is what lifts the client's access), and `null` otherwise: the MSSP's end date\n and whether its beta ended or converted are not the client's to read. The tenant's\n own organisation's beta is described as before. No field was added or removed.\n\n- **2026-10-21 — addition: `user` on `GET /v1/me`.** The `whoAmI` answer gains\n a nullable `user` object (`id`, `name`, `email`): the signed-in user of an\n OAuth access token (`hs_at_…`), so a client can show who is logged in. It is the\n caller's own data only, never another user's. It is `null` for a tenant API key\n and for a portfolio key. Nothing existing was removed, renamed or re-statused.\n\n- **2026-10-20 — addition: `beta` on `GET /v1/me`.** The `whoAmI` answer gains\n a nullable `beta` object (`status`, `running`, `ends_at`): the beta of the\n tenant's own organisation, so a client can show when it ends. It is `null` for an\n organisation that never joined the beta and for a portfolio key; a beta inherited\n from a parent MSSP is not described. Nothing existing was removed, renamed or\n re-statused.\n\n- **2026-10-19 — addition: reports.** `GET /v1/reports` (`listReports`),\n `GET /v1/reports/{id}` (`getReport`) and\n `GET /v1/reports/{id}/download` (`downloadReport`), all under the scope\n `reports:read`. The download is the contract's first non-JSON success: a\n PDF or XLSX file as an attachment (see Downloads). A new `error.code`,\n `conflict` (**409**), answered only by the download, with three new\n `error.details.code` values (`REPORT_NOT_READY`, `FORMAT_UNSUPPORTED`,\n `FILE_TOO_LARGE`). Nothing existing was removed, renamed or re-statused.\n\n- **2026-10-18 — addition: known values for four filters.** `GET /v1/risks`\n `domain` and `status`, `GET /v1/compliance/controls` `status` and the\n `vendor_type` field of `GET /v1/vendors` now list their vocabulary in\n `x-hs-known-values`, taken from the lists the platform validates against.\n They stay open: a value outside the list is still accepted and matches exactly,\n as before. `GET /v1/vendors` `category` is free text with no closed\n vocabulary, and its description now says so. Nothing existing was removed,\n renamed or re-statused.\n\n- **2026-10-17 — addition: `GET /v1/me`.** The operation `whoAmI` describes the\n calling credential: its type and scopes, the tenant (id, name) and organisation it\n reads, or, for a portfolio key, the MSSP organisation and how many tenants it can\n reach. Any valid credential may call it and it requires no scope; it uses the same\n authentication and rate limits as every other operation, and refuses\n `X-HS-Tenant`. Nothing existing was removed, renamed or re-statused.\n\n- **2026-10-16 — addition: MSSP portfolio keys.** A third credential, the\n portfolio key (`hsp_…`); the operation `GET /v1/portfolio/tenants`\n (`listPortfolioTenants`, scope `portfolio:read`); the optional request\n header `X-HS-Tenant` on every data operation, echoed on every 2xx answer of\n them for every credential, with `Vary: X-HS-Tenant`;\n `error.details.code` becomes contract (`TENANT_HEADER_INVALID`,\n `TENANT_REQUIRED`, `TENANT_SELECTOR_NOT_SUPPORTED`, `TENANT_NOT_FOUND`,\n `INSUFFICIENT_SCOPE`, `LICENCE_INACTIVE`), and every scope 403 now carries\n `INSUFFICIENT_SCOPE`; three new 429 `details.scope` values\n (`portfolio_key`, `portfolio_org`, `portfolio_tenant`). Nothing existing\n was removed, renamed or re-statused.\n\n- **2026-10-16 — correction: alert `severity` is English.** `GET /v1/alerts`\n and `GET /v1/alerts/{id}` returned `severity` as stored, in Spanish\n (`critico`, `alto`, `medio`, `bajo`, `info`), while this document\n promised `critical | high | medium | low`. They now return `critical`,\n `high`, `medium`, `low` and `info` (`info` is added to the documented\n values). **This changes what an integrator who adapted to the Spanish values\n receives.** The `severity` filter accepts both the English values and the\n Spanish ones; the Spanish ones are documented as accepted until\n **2027-01-14** (90 days after this version), so switch to the English ones\n before then. A value outside both lists is still matched exactly and matches\n nothing. `GET /v1/risks`: the descriptions of `domain` and `status` named\n values that do not exist; they now list the real ones (no value changed).","contact":{"name":"Hodeitek S.L.","url":"https://hodeishield.com"}},"servers":[{"url":"https://api.hodeishield.com"}],"tags":[{"name":"vendors"},{"name":"alerts"},{"name":"risks"},{"name":"compliance"},{"name":"evidence"},{"name":"endpoints"},{"name":"Portfolio"},{"name":"Identity"},{"name":"reports"}],"paths":{"/v1/vendors":{"get":{"operationId":"listVendors","summary":"List third parties","description":"The tenant third-party inventory, newest first by default. Soft-deleted vendors are never returned.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["vendors"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, updated_at, name.","schema":{"default":"created_at","type":"string","enum":["created_at","updated_at","name"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring of the name or the domain.","schema":{"type":"string","minLength":1,"maxLength":120}},{"name":"category","in":"query","required":false,"description":"Exact category match, as the tenant wrote it (case-sensitive). Category is free text with NO closed vocabulary, so there is no list of values to publish: read `category` from `GET /v1/vendors`. To filter by the kind of third party, read `vendor_type` (`x-hs-known-values`).","schema":{"type":"string","minLength":1,"maxLength":120}},{"name":"active","in":"query","required":false,"description":"Filter by the operational flag.","schema":{"type":"string"}},{"name":"business_criticality","in":"query","required":false,"description":"Exact criticality match.","schema":{"type":"string","enum":["low","medium","high","critical"]}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Stable identifier of the vendor in this tenant."},"name":{"type":"string","description":"Display name."},"legal_name":{"description":"Registered legal name, when it differs.","type":["string","null"]},"domain":{"description":"Primary domain, lowercase, no scheme.","type":["string","null"]},"category":{"description":"Free-text category assigned by the tenant. NOT a closed vocabulary: the console form stores the label of the tenant’s language, and the API accepts any text. List the vendors to see the values a tenant uses.","type":["string","null"]},"vendor_type":{"description":"Kind of third party: software | cloud | hardware | service_provider. Open vocabulary: a new kind is a compatible change.","x-hs-known-values":["software","cloud","hardware","service_provider"],"type":["string","null"]},"access_level":{"description":"Level of access the vendor has to the tenant systems.","type":["string","null"]},"business_criticality":{"description":"low | medium | high | critical, as set by the tenant.","type":["string","null"]},"data_sensitivity":{"description":"public | internal | confidential | restricted.","type":["string","null"]},"economic_impact":{"description":"Estimated economic impact of an outage, in the tenant currency.","type":["number","null"]},"active":{"type":"boolean","description":"Whether the vendor is operationally enabled."},"monitoring_status":{"description":"Continuous-monitoring state.","type":["string","null"]},"last_scan_at":{"description":"RFC 3339, UTC. Null if never scanned.","type":["string","null"]},"lei":{"description":"Legal Entity Identifier (ISO 17442), for DORA RoI.","type":["string","null"]},"hq_country":{"description":"Head-office country, ISO 3166-1 alpha-2.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","name","legal_name","domain","category","vendor_type","access_level","business_criticality","data_sensitivity","economic_impact","active","monitoring_status","last_scan_at","lei","hq_country","created_at","updated_at"],"additionalProperties":false,"description":"A third party in the tenant inventory."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/vendors/{id}":{"get":{"operationId":"getVendor","summary":"Fetch one third party","description":"The same object `listVendors` returns, for a single vendor.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["vendors"],"parameters":[{"name":"id","in":"path","required":true,"description":"Vendor identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Stable identifier of the vendor in this tenant."},"name":{"type":"string","description":"Display name."},"legal_name":{"description":"Registered legal name, when it differs.","type":["string","null"]},"domain":{"description":"Primary domain, lowercase, no scheme.","type":["string","null"]},"category":{"description":"Free-text category assigned by the tenant. NOT a closed vocabulary: the console form stores the label of the tenant’s language, and the API accepts any text. List the vendors to see the values a tenant uses.","type":["string","null"]},"vendor_type":{"description":"Kind of third party: software | cloud | hardware | service_provider. Open vocabulary: a new kind is a compatible change.","x-hs-known-values":["software","cloud","hardware","service_provider"],"type":["string","null"]},"access_level":{"description":"Level of access the vendor has to the tenant systems.","type":["string","null"]},"business_criticality":{"description":"low | medium | high | critical, as set by the tenant.","type":["string","null"]},"data_sensitivity":{"description":"public | internal | confidential | restricted.","type":["string","null"]},"economic_impact":{"description":"Estimated economic impact of an outage, in the tenant currency.","type":["number","null"]},"active":{"type":"boolean","description":"Whether the vendor is operationally enabled."},"monitoring_status":{"description":"Continuous-monitoring state.","type":["string","null"]},"last_scan_at":{"description":"RFC 3339, UTC. Null if never scanned.","type":["string","null"]},"lei":{"description":"Legal Entity Identifier (ISO 17442), for DORA RoI.","type":["string","null"]},"hq_country":{"description":"Head-office country, ISO 3166-1 alpha-2.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","name","legal_name","domain","category","vendor_type","access_level","business_criticality","data_sensitivity","economic_impact","active","monitoring_status","last_scan_at","lei","hq_country","created_at","updated_at"],"additionalProperties":false,"description":"A third party in the tenant inventory."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/alerts":{"get":{"operationId":"listAlerts","summary":"List supply-chain alerts","description":"Alerts raised against the tenant vendors. Poll new alerts with `since` plus `order=asc`, changed ones with `updated_since` plus `sort=updated_at&order=asc`.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["alerts"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1. Cannot be combined with `cursor`.","schema":{"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","in":"query","required":false,"description":"Opaque keyset cursor: pass the `next_cursor` of the previous page, with the same `sort`, `order` and filters. Cannot be combined with `page`.","schema":{"type":"string","minLength":1,"maxLength":512}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, updated_at.","schema":{"default":"created_at","type":"string","enum":["created_at","updated_at"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"status","in":"query","required":false,"description":"Filter by the derived status.","schema":{"type":"string","enum":["open","acknowledged","resolved"]}},{"name":"severity","in":"query","required":false,"description":"Exact severity match, in the vocabulary the response uses (critical | high | medium | low | info). The legacy Spanish values (critico, alto, medio, bajo, info) are still accepted and mean the same; they are documented as accepted until 2027-01-14, so move to the English ones.","schema":{"x-hs-known-values":["critical","high","medium","low","info"],"x-hs-deprecated-values":["critico","alto","medio","bajo"],"x-hs-deprecated-values-sunset":"2027-01-14","type":"string","minLength":1,"maxLength":40}},{"name":"vendor_id","in":"query","required":false,"description":"Only alerts about this vendor.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"since","in":"query","required":false,"description":"Only alerts created at or after this RFC 3339 instant.","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},{"name":"updated_since","in":"query","required":false,"description":"Only alerts last changed at or after this RFC 3339 instant (`updated_at`), including ones created long before it. Poll changes with `sort=updated_at&order=asc`.","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"vendor_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The vendor this alert is about (`/v1/vendors/{id}`)."},"type":{"type":"string","description":"Alert family, e.g. `breach`, `cve`, `rating_drop`."},"severity":{"description":"critical | high | medium | low | info, when classified. Open vocabulary: a new value is a compatible change.","x-hs-known-values":["critical","high","medium","low","info"],"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"source_url":{"description":"Where the finding came from, when public.","type":["string","null"]},"economic_impact":{"type":["number","null"]},"status":{"type":"string","enum":["open","acknowledged","resolved"],"description":"Derived: resolved if closed, acknowledged if taken, otherwise open."},"acknowledged_at":{"description":"RFC 3339, UTC.","type":["string","null"]},"resolved_at":{"description":"RFC 3339, UTC.","type":["string","null"]},"resolution_reason":{"description":"Why it was closed, when it was.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC. Last change of severity, acknowledgement, resolution or CISA KEV status; equals the instant the service started tracking it for alerts older than that."}},"required":["id","vendor_id","type","severity","title","description","source_url","economic_impact","status","acknowledged_at","resolved_at","resolution_reason","created_at","updated_at"],"additionalProperties":false,"description":"A supply-chain alert raised against one vendor."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."},"next_cursor":{"description":"Pass as `cursor` for the next page; null on the last page.","type":["string","null"]}},"required":["per_page","next_cursor"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/alerts/{id}":{"get":{"operationId":"getAlert","summary":"Fetch one alert","description":"The same object `listAlerts` returns, for a single alert.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["alerts"],"parameters":[{"name":"id","in":"path","required":true,"description":"Alert identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"vendor_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The vendor this alert is about (`/v1/vendors/{id}`)."},"type":{"type":"string","description":"Alert family, e.g. `breach`, `cve`, `rating_drop`."},"severity":{"description":"critical | high | medium | low | info, when classified. Open vocabulary: a new value is a compatible change.","x-hs-known-values":["critical","high","medium","low","info"],"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"source_url":{"description":"Where the finding came from, when public.","type":["string","null"]},"economic_impact":{"type":["number","null"]},"status":{"type":"string","enum":["open","acknowledged","resolved"],"description":"Derived: resolved if closed, acknowledged if taken, otherwise open."},"acknowledged_at":{"description":"RFC 3339, UTC.","type":["string","null"]},"resolved_at":{"description":"RFC 3339, UTC.","type":["string","null"]},"resolution_reason":{"description":"Why it was closed, when it was.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC. Last change of severity, acknowledgement, resolution or CISA KEV status; equals the instant the service started tracking it for alerts older than that."}},"required":["id","vendor_id","type","severity","title","description","source_url","economic_impact","status","acknowledged_at","resolved_at","resolution_reason","created_at","updated_at"],"additionalProperties":false,"description":"A supply-chain alert raised against one vendor."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/risks":{"get":{"operationId":"listRisks","summary":"List risk-register entries","description":"The tenant risk register, with inherent and residual figures.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["risks"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, updated_at, inherent_score, residual_score.","schema":{"default":"created_at","type":"string","enum":["created_at","updated_at","inherent_score","residual_score"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"status","in":"query","required":false,"description":"Exact status match. Known values: identified, assessed, treatment_planned, treatment_implemented, closed. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["identified","assessed","treatment_planned","treatment_implemented","closed"],"type":"string","minLength":1,"maxLength":24}},{"name":"domain","in":"query","required":false,"description":"Exact domain match. Known values: vendor, cyber, data, operational, compliance, concentration, strategic, legal, financial, reputational. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["vendor","cyber","data","operational","compliance","concentration","strategic","legal","financial","reputational"],"type":"string","minLength":1,"maxLength":20}},{"name":"vendor_id","in":"query","required":false,"description":"Only risks linked to this vendor.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"min_inherent_score","in":"query","required":false,"description":"Only risks whose inherent score is at least this.","schema":{"type":"integer","minimum":0,"maximum":9007199254740991}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"title":{"type":"string"},"description":{"type":["string","null"]},"domain":{"type":"string","description":"Risk domain: vendor | cyber | data | operational | compliance | concentration | strategic | legal | financial | reputational.","x-hs-known-values":["vendor","cyber","data","operational","compliance","concentration","strategic","legal","financial","reputational"]},"status":{"type":"string","description":"Risk lifecycle status: identified | assessed | treatment_planned | treatment_implemented | closed.","x-hs-known-values":["identified","assessed","treatment_planned","treatment_implemented","closed"]},"treatment":{"description":"mitigate | accept | transfer | avoid.","type":["string","null"]},"treatment_notes":{"type":["string","null"]},"threat_source":{"type":["string","null"]},"vulnerability":{"type":["string","null"]},"likelihood_inherent":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"impact_inherent":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"inherent_score":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"likelihood_residual":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"impact_residual":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"residual_score":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"source":{"type":"string","description":"manual | imported | derived."},"owner_user_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"User of this tenant who owns the risk."},"vendor_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The vendor this risk is about, when any."},"control_codes":{"type":"array","items":{"type":"string"},"description":"Framework control codes linked to the risk."},"review_date":{"description":"Calendar date, `YYYY-MM-DD`.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","title","description","domain","status","treatment","treatment_notes","threat_source","vulnerability","likelihood_inherent","impact_inherent","inherent_score","likelihood_residual","impact_residual","residual_score","source","owner_user_id","vendor_id","control_codes","review_date","created_at","updated_at"],"additionalProperties":false,"description":"An entry of the tenant risk register."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/risks/{id}":{"get":{"operationId":"getRisk","summary":"Fetch one risk-register entry","description":"The same object `listRisks` returns, for a single entry.\n\nRequired scope: `supply_risk:read`. Published in `/v1` since 2026-09-20.","tags":["risks"],"parameters":[{"name":"id","in":"path","required":true,"description":"Risk identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"title":{"type":"string"},"description":{"type":["string","null"]},"domain":{"type":"string","description":"Risk domain: vendor | cyber | data | operational | compliance | concentration | strategic | legal | financial | reputational.","x-hs-known-values":["vendor","cyber","data","operational","compliance","concentration","strategic","legal","financial","reputational"]},"status":{"type":"string","description":"Risk lifecycle status: identified | assessed | treatment_planned | treatment_implemented | closed.","x-hs-known-values":["identified","assessed","treatment_planned","treatment_implemented","closed"]},"treatment":{"description":"mitigate | accept | transfer | avoid.","type":["string","null"]},"treatment_notes":{"type":["string","null"]},"threat_source":{"type":["string","null"]},"vulnerability":{"type":["string","null"]},"likelihood_inherent":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"impact_inherent":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"inherent_score":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"likelihood_residual":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"impact_residual":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"residual_score":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"source":{"type":"string","description":"manual | imported | derived."},"owner_user_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"User of this tenant who owns the risk."},"vendor_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The vendor this risk is about, when any."},"control_codes":{"type":"array","items":{"type":"string"},"description":"Framework control codes linked to the risk."},"review_date":{"description":"Calendar date, `YYYY-MM-DD`.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"updated_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","title","description","domain","status","treatment","treatment_notes","threat_source","vulnerability","likelihood_inherent","impact_inherent","inherent_score","likelihood_residual","impact_residual","residual_score","source","owner_user_id","vendor_id","control_codes","review_date","created_at","updated_at"],"additionalProperties":false,"description":"An entry of the tenant risk register."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["supply_risk:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["supply_risk:read"]}}},"/v1/compliance/controls":{"get":{"operationId":"listComplianceControls","summary":"List control status for a framework","description":"Per-control status of one framework. Requires the read scope OF THAT FRAMEWORK: `compliance.:read`.\n\nRequired scope: `compliance.:read`. Published in `/v1` since 2026-09-20.","tags":["compliance"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: control_code, last_evaluated_at.","schema":{"default":"control_code","type":"string","enum":["control_code","last_evaluated_at"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"framework","in":"query","required":true,"description":"Framework slug. One of: nis2, ens, iso27001, dora, gdpr, soc2, iso42001, nistcsf, pcidss, euaiact, hipaa, cmmc, cyberessentials, uaeia, sama, saudiecc, ndpa.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Exact status match. Known values: covered, partial, missing. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["covered","partial","missing"],"type":"string","minLength":1,"maxLength":20}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"framework":{"type":"string","description":"Framework slug the control belongs to."},"control_code":{"type":"string","description":"Control identifier inside the framework."},"status":{"type":"string","description":"covered | partial | missing, as evaluated by the platform.","x-hs-known-values":["covered","partial","missing"]},"evidence_count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Evidence items currently linked to the control."},"last_evaluated_at":{"description":"RFC 3339, UTC.","type":["string","null"]}},"required":["framework","control_code","status","evidence_count","last_evaluated_at"],"additionalProperties":false,"description":"The tenant status of one framework control."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":[]},{"portfolioKey":[]}],"x-hs-scope":{"template":"compliance.:read"}}},"/v1/compliance/frameworks/{framework}":{"get":{"operationId":"getFrameworkPosture","summary":"Framework posture summary","description":"Counts of evaluated controls by status for one framework, for a dashboard tile. Requires `compliance.:read`.\n\nRequired scope: `compliance.:read`. Published in `/v1` since 2026-09-20.","tags":["compliance"],"parameters":[{"name":"framework","in":"path","required":true,"description":"Framework slug. One of: nis2, ens, iso27001, dora, gdpr, soc2, iso42001, nistcsf, pcidss, euaiact, hipaa, cmmc, cyberessentials, uaeia, sama, saudiecc, ndpa.","schema":{"type":"string"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"framework":{"type":"string"},"controls_total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Controls the platform has evaluated."},"by_status":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":"Control count per status. Keys are whatever statuses exist; may grow."},"evidence_total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Sum of the evidence linked across the evaluated controls."},"last_evaluated_at":{"description":"Most recent control evaluation, RFC 3339 UTC. Null if never evaluated.","type":["string","null"]}},"required":["framework","controls_total","by_status","evidence_total","last_evaluated_at"],"additionalProperties":false,"description":"Posture summary for one framework."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":[]},{"portfolioKey":[]}],"x-hs-scope":{"template":"compliance.:read"}}},"/v1/evidence":{"get":{"operationId":"listEvidence","summary":"List security evidence","description":"Evidence held for the tenant. Use `expiring_before` plus `sort=valid_until&order=asc` for a renewal queue.\n\nRequired scope: `evidence:read`. Published in `/v1` since 2026-09-20.","tags":["evidence"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, valid_until.","schema":{"default":"created_at","type":"string","enum":["created_at","valid_until"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"evidence_type","in":"query","required":false,"description":"Exact type match.","schema":{"type":"string","minLength":1,"maxLength":50}},{"name":"source_type","in":"query","required":false,"description":"Exact producer match.","schema":{"type":"string","minLength":1,"maxLength":50}},{"name":"vendor_id","in":"query","required":false,"description":"Only evidence about this vendor.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"expiring_before","in":"query","required":false,"description":"Only evidence with a `valid_until` strictly before this `YYYY-MM-DD`.","schema":{"type":"string","format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"evidence_type":{"type":"string","description":"What kind of evidence this is."},"source_type":{"type":"string","description":"Which subsystem produced it."},"source_id":{"type":"string","description":"Its identifier inside that subsystem."},"title":{"type":"string"},"description":{"type":["string","null"]},"vendor_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The vendor it is about, when any."},"asset_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The asset it is about, when any."},"auto_generated":{"type":"boolean","description":"False when a person uploaded it."},"valid_until":{"description":"Calendar date, `YYYY-MM-DD`. Null = no expiry.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","evidence_type","source_type","source_id","title","description","vendor_id","asset_id","auto_generated","valid_until","created_at"],"additionalProperties":false,"description":"One item of security evidence."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["evidence:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["evidence:read"]}}},"/v1/evidence/{id}":{"get":{"operationId":"getEvidence","summary":"Fetch one evidence item","description":"The same object `listEvidence` returns, for a single item.\n\nRequired scope: `evidence:read`. Published in `/v1` since 2026-09-20.","tags":["evidence"],"parameters":[{"name":"id","in":"path","required":true,"description":"Evidence identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"evidence_type":{"type":"string","description":"What kind of evidence this is."},"source_type":{"type":"string","description":"Which subsystem produced it."},"source_id":{"type":"string","description":"Its identifier inside that subsystem."},"title":{"type":"string"},"description":{"type":["string","null"]},"vendor_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The vendor it is about, when any."},"asset_id":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}],"description":"The asset it is about, when any."},"auto_generated":{"type":"boolean","description":"False when a person uploaded it."},"valid_until":{"description":"Calendar date, `YYYY-MM-DD`. Null = no expiry.","type":["string","null"]},"created_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","evidence_type","source_type","source_id","title","description","vendor_id","asset_id","auto_generated","valid_until","created_at"],"additionalProperties":false,"description":"One item of security evidence."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["evidence:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["evidence:read"]}}},"/v1/endpoints":{"get":{"operationId":"listEndpoints","summary":"List enrolled endpoint agents","description":"The endpoint agents enrolled in this tenant. `silent_since` selects the ones that have gone quiet, including those that never reported.\n\nRequired scope: `endpoints:read`. Published in `/v1` since 2026-09-20.","tags":["endpoints"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: enrolled_at, last_seen_at, producer.","schema":{"default":"enrolled_at","type":"string","enum":["enrolled_at","last_seen_at","producer"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"status","in":"query","required":false,"description":"Exact status match.","schema":{"type":"string","enum":["active","revoked"]}},{"name":"silent_since","in":"query","required":false,"description":"Agents last seen before this RFC 3339 instant, or never seen at all.","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"producer":{"type":"string","description":"The agent name, unique among the active ones of the tenant."},"producer_kind":{"type":"string","description":"What kind of producer it is."},"status":{"type":"string","description":"active | revoked."},"signature_alg":{"type":"string","description":"Algorithm this enrollment signs with."},"agent_version":{"type":["string","null"]},"hostname":{"description":"Last hostname the agent reported.","type":["string","null"]},"enrolled_at":{"type":"string","description":"RFC 3339, UTC."},"last_seen_at":{"description":"RFC 3339, UTC. Null until the first envelope.","type":["string","null"]},"revoked_at":{"description":"RFC 3339, UTC.","type":["string","null"]}},"required":["id","producer","producer_kind","status","signature_alg","agent_version","hostname","enrolled_at","last_seen_at","revoked_at"],"additionalProperties":false,"description":"An endpoint agent enrolled in this tenant."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["endpoints:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["endpoints:read"]}}},"/v1/portfolio/tenants":{"get":{"operationId":"listPortfolioTenants","summary":"List the client tenants of an MSSP portfolio","description":"Every tenant the presented PORTFOLIO key may select with `X-HS-Tenant`: the MSSP’s own tenants (its internal one included) and every tenant of its active client organisations, active or not, licensed or not, narrowed to the key’s client subset when it has one. Membership is evaluated live on every request. Only a portfolio key (`hsp_…`) can call this operation; this operation never takes `X-HS-Tenant`.\n\nRequired scope: `portfolio:read`. Published in `/v1` since 2026-10-16.","tags":["Portfolio"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: name, slug.","schema":{"default":"name","type":"string","enum":["name","slug"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Stable identifier (lowercase). The ONLY value `X-HS-Tenant` accepts."},"name":{"type":"string","description":"Display name. Untrusted text."},"slug":{"type":"string","description":"Short name, unique across the platform. A client may resolve a slug to an `id` from this list; the server never accepts a slug as a selector."},"organization":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"description":"The organisation the tenant belongs to: the MSSP itself for the tenants it holds directly (including its internal one), or the client organisation."},"internal":{"type":"boolean","description":"True only for the MSSP’s own internal tenant."},"active":{"type":"boolean","description":"Whether the tenant is enabled. A deactivated tenant is listed but cannot be read."},"licence_status":{"type":"string","description":"Open vocabulary: `in_force` (licence in force), `grace` (grace period: reads are served), `inactive` (none, suspended, revoked or expired: data operations answer 403 `LICENCE_INACTIVE`). Treat an unknown value as \"may not be readable\". A tenant is readable when `active` is true and `licence_status` is not `inactive`.","x-hs-known-values":["in_force","grace","inactive"]}},"required":["id","name","slug","organization","internal","active","licence_status"],"additionalProperties":false,"description":"A client tenant an MSSP portfolio key may select."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"portfolioKey":[]}],"x-hs-scope":{"scopes":["portfolio:read"]}}},"/v1/me":{"get":{"operationId":"whoAmI","summary":"Describe the calling credential","description":"Who the presented credential is: its type and scopes, and the tenant and organisation it reads (a tenant API key or an OAuth token), for an OAuth token also the signed-in user (`user`: its own id, name and email, never another user’s; null for any key), or the MSSP organisation it belongs to and how many tenants it can reach (a portfolio key; the tenants themselves are on `GET /v1/portfolio/tenants`). It describes the caller only: no other tenant, no secret, never the key or its hash. Any valid credential may call it: it requires NO scope, and it uses the same authentication and the same rate limits as every other operation. It never takes `X-HS-Tenant`.\n\nRequired scope: `none (any valid credential)`. Published in `/v1` since 2026-10-17.","tags":["Identity"],"parameters":[],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"credential":{"type":"object","properties":{"type":{"type":"string","description":"Open vocabulary: `tenant` (a tenant API key, `hsk_…`), `oauth` (a user access token, `hs_at_…`) or `portfolio` (an MSSP portfolio key, `hsp_…`).","x-hs-known-values":["tenant","oauth","portfolio"]},"prefix":{"description":"The clear key prefix (`hsp_…`) of a portfolio key. Null for a tenant key or an OAuth token. Never the full key and never a hash.","type":["string","null"]},"scopes":{"type":"array","items":{"type":"string"},"description":"The scopes the credential carries."}},"required":["type","prefix","scopes"],"additionalProperties":false,"description":"The credential that made this call."},"tenant":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The tenant this credential reads (lowercase)."},"name":{"type":"string","description":"Display name. Untrusted text."},"type":{"type":"string","description":"The type of the organisation that owns the tenant. Open vocabulary: `direct` (a customer organisation), `mssp` (a managed security service provider) or `root` (the platform operator). Treat an unknown value as opaque.","x-hs-known-values":["direct","mssp","root"]}},"required":["id","name","type"],"additionalProperties":false},{"type":"null"}],"description":"The credential’s own tenant. Null for a portfolio key, which has no tenant."},"organization":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"description":"Display name. Null for a portfolio key whose client subset holds none of the provider’s own tenants, the only place a portfolio key sees the name. Untrusted text.","type":["string","null"]},"type":{"type":"string","description":"`direct`, `mssp` or `root`; `mssp` for a portfolio key."}},"required":["id","name","type"],"additionalProperties":false,"description":"The organisation of the tenant (tenant key, OAuth token) or the MSSP the key belongs to (portfolio key)."},"beta":{"anyOf":[{"type":"object","properties":{"status":{"type":"string","description":"The beta status of the tenant’s own organisation, or `active` while the beta of its parent MSSP runs and lifts its access. Open vocabulary: `active`, `ended` or `converted`.","x-hs-known-values":["active","ended","converted"]},"running":{"type":"boolean","description":"True while the beta runs: status `active` and `ends_at` in the future. While it runs the organisation has Enterprise access."},"ends_at":{"description":"When the own organisation’s beta ends or ended, ISO 8601 UTC. Null when no end is recorded, and always null for a beta inherited from the parent MSSP.","type":["string","null"]}},"required":["status","running","ends_at"],"additionalProperties":false},{"type":"null"}],"description":"The beta of the tenant’s own organisation; while it does not run and the parent MSSP’s beta does, `active` and running with no date. Nothing else about the parent’s beta is described: not its end, nor whether it ended or converted. Null when the own organisation never joined the beta and no parent beta runs, and for a portfolio key."},"user":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"The user the access token belongs to (lowercase)."},"name":{"description":"The user’s display name. Untrusted text.","type":["string","null"]},"email":{"type":"string","description":"The user’s email address."}},"required":["id","name","email"],"additionalProperties":false},{"type":"null"}],"description":"The signed-in user of an OAuth access token (`hs_at_…`): the CALLER’S OWN id, name and email, never another user’s. Null for a tenant API key and for a portfolio key, which act for no user, and for a token whose user row is no longer visible in the token’s tenant."},"portfolio":{"anyOf":[{"type":"object","properties":{"tenant_count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many tenants this key can select, right now."},"tenants_path":{"type":"string","description":"Where the tenants are listed: `GET /v1/portfolio/tenants`."}},"required":["tenant_count","tenants_path"],"additionalProperties":false},{"type":"null"}],"description":"Portfolio keys only. Null for a tenant key or an OAuth token."}},"required":["credential","tenant","organization","beta","user","portfolio"],"additionalProperties":false,"description":"The credential that made the call, and what it may see about itself."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":[]},{"portfolioKey":[]}],"x-hs-scope":{"none":true}}},"/v1/reports":{"get":{"operationId":"listReports","summary":"List generated reports","description":"The reports generated for the tenant, newest first by default. Poll for new ones with `since` plus `order=asc`; download one with `downloadReport` once its `status` is `completed`.\n\nRequired scope: `reports:read`. Published in `/v1` since 2026-10-19.","tags":["reports"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number. Defaults to 1.","schema":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},{"name":"per_page","in":"query","required":false,"description":"Items per page, 1..200. Defaults to 50.","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"sort","in":"query","required":false,"description":"Field to order by. One of: created_at, period_end.","schema":{"default":"created_at","type":"string","enum":["created_at","period_end"]}},{"name":"order","in":"query","required":false,"description":"Sort direction. Defaults to desc.","schema":{"default":"desc","type":"string","enum":["asc","desc"]}},{"name":"report_type","in":"query","required":false,"description":"Exact type match. Known values: monthly, vendor_risk, nis2_compliance, ens, cyberbrief, compliance, security_posture, board_report. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["monthly","vendor_risk","nis2_compliance","ens","cyberbrief","compliance","security_posture","board_report"],"type":"string","minLength":1,"maxLength":50}},{"name":"status","in":"query","required":false,"description":"Exact status match. Known values: generating, completed, failed. Open vocabulary: any other value is matched exactly and matches nothing.","schema":{"x-hs-known-values":["generating","completed","failed"],"type":"string","minLength":1,"maxLength":20}},{"name":"since","in":"query","required":false,"description":"Only reports created at or after this RFC 3339 instant.","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"report_type":{"type":"string","description":"What kind of report this is. Known values: monthly, vendor_risk, nis2_compliance, ens, cyberbrief, compliance, security_posture, board_report. Open vocabulary: a new kind may appear.","x-hs-known-values":["monthly","vendor_risk","nis2_compliance","ens","cyberbrief","compliance","security_posture","board_report"]},"title":{"type":"string"},"period_start":{"type":"string","description":"First day the report covers, `YYYY-MM-DD`."},"period_end":{"type":"string","description":"Last day the report covers, `YYYY-MM-DD`."},"status":{"type":"string","description":"Lifecycle: generating | completed | failed. Only a `completed` report can be downloaded.","x-hs-known-values":["generating","completed","failed"]},"created_at":{"type":"string","description":"RFC 3339, UTC."}},"required":["id","report_type","title","period_start","period_end","status","created_at"],"additionalProperties":false,"description":"A report the platform generated for the tenant (metadata only)."}},"pagination":{"type":"object","properties":{"page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The page that was served."},"per_page":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Items per page in effect."},"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total items matching the filters, across all pages."},"total_pages":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of pages at this `per_page`."}},"required":["page","per_page","total","total_pages"],"additionalProperties":false}},"required":["data","pagination"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["reports:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["reports:read"]}}},"/v1/reports/{id}":{"get":{"operationId":"getReport","summary":"Fetch one report","description":"The same object `listReports` returns, for a single report, plus the formats it can be downloaded in now (`download_formats`).\n\nRequired scope: `reports:read`. Published in `/v1` since 2026-10-19.","tags":["reports"],"parameters":[{"name":"id","in":"path","required":true,"description":"Report identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"report_type":{"type":"string","description":"What kind of report this is. Known values: monthly, vendor_risk, nis2_compliance, ens, cyberbrief, compliance, security_posture, board_report. Open vocabulary: a new kind may appear.","x-hs-known-values":["monthly","vendor_risk","nis2_compliance","ens","cyberbrief","compliance","security_posture","board_report"]},"title":{"type":"string"},"period_start":{"type":"string","description":"First day the report covers, `YYYY-MM-DD`."},"period_end":{"type":"string","description":"Last day the report covers, `YYYY-MM-DD`."},"status":{"type":"string","description":"Lifecycle: generating | completed | failed. Only a `completed` report can be downloaded.","x-hs-known-values":["generating","completed","failed"]},"created_at":{"type":"string","description":"RFC 3339, UTC."},"download_formats":{"type":"array","items":{"type":"string","description":"A format `downloadReport` serves. Known values: pdf, xlsx. Open vocabulary: a format may be added.","x-hs-known-values":["pdf","xlsx"]},"description":"The formats `downloadReport` can serve for this report now. Empty while it is not `completed`, or when its content has no renderer (download it from the console as Markdown or JSON)."}},"required":["id","report_type","title","period_start","period_end","status","created_at","download_formats"],"additionalProperties":false,"description":"One report, with the formats it can be downloaded in."}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["reports:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["reports:read"]}}},"/v1/reports/{id}/download":{"get":{"operationId":"downloadReport","summary":"Download a report file","description":"The report rendered as a file (PDF or XLSX), returned as the response body with `Content-Disposition: attachment`; never a link. Each delivery is recorded in the tenant’s audit trail with the credential that received it. A report that is not `completed`, a format its content has no renderer for (see `download_formats` in `getReport`) and a file over the size cap are a 409.\n\nRequired scope: `reports:read`. Published in `/v1` since 2026-10-19.","tags":["reports"],"parameters":[{"name":"id","in":"path","required":true,"description":"Report identifier.","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"format","in":"query","required":false,"description":"File format, one of: pdf, xlsx. Defaults to pdf. `getReport` lists the formats a report can be served in (`download_formats`).","schema":{"default":"pdf","type":"string","enum":["pdf","xlsx"]}},{"name":"locale","in":"query","required":false,"description":"Language of the file, one of: es, en, de, fr, it, eu. Defaults to en.","schema":{"default":"en","type":"string","enum":["es","en","de","fr","it","eu"]}},{"name":"X-HS-Tenant","in":"header","required":false,"description":"The tenant to read, as its id (a UUID, any case). REQUIRED with a portfolio key (`hsp_…`); with a tenant API key or an OAuth token, optional and accepted only when it equals the credential’s own tenant. Never a slug.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The file, as an attachment. At most 20971520 bytes; a larger one is a 409 `FILE_TOO_LARGE`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"X-HS-Tenant":{"description":"The id (lowercase UUID) of the tenant whose data this body holds, for every credential. Present on every 2xx answer of a data operation and on no error. A client that selected a tenant must refuse a 2xx whose echo is missing or different.","schema":{"type":"string","format":"uuid"}},"Content-Disposition":{"description":"`attachment; filename=\".\"`. The name is `[A-Za-z0-9._-]`.","schema":{"type":"string"}},"Content-Length":{"description":"Size of the file in bytes.","schema":{"type":"integer","minimum":0}}},"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"A parameter is missing, malformed or out of range: `error.details.issues` names it. Or the `X-HS-Tenant` header was refused (`error.details.parameter` is `X-HS-Tenant`): `TENANT_HEADER_INVALID` (not exactly one UUID), `TENANT_REQUIRED` (a portfolio key on a data operation without it) or `TENANT_SELECTOR_NOT_SUPPORTED` (on `listPortfolioTenants`, or a tenant API key / OAuth token naming a tenant other than its own) in `error.details.code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential, or one that is unknown, malformed, revoked or expired (a tenant API key, an OAuth access token or a portfolio key; the cases are not distinguished), a portfolio key whose organisation is no longer an active MSSP, or a portfolio key used from an address outside its allowed ranges. The `WWW-Authenticate` header carries `resource_metadata`, the RFC 9728 document that names the authorization server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The credential authenticated but may not perform this operation. Either it does not carry the scope the operation requires (`error.details.code` is `INSUFFICIENT_SCOPE` and `error.details.missing` lists the scopes), or the tenant read (the credential’s own, or the one a portfolio key selected) is deactivated or holds no licence in force: none, suspended, revoked or expired, or a write during the grace period (`error.details.code` is `LICENCE_INACTIVE` and `error.details.licence` says which). Re-authenticating does not help with the second.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such resource in this tenant. With a portfolio key, also: the selected tenant does not exist or is not in this key’s portfolio (`error.details.code` is `TENANT_NOT_FOUND`; the two cases are one answer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The file cannot be served as asked. `error.details.code` says why: `REPORT_NOT_READY` (the report is still `generating`, or `failed`; `error.details.status` says which. Retry a `generating` one later); `FORMAT_UNSUPPORTED` (the report’s content has no renderer for that format; `error.details.formats` lists the ones it has, possibly none); `FILE_TOO_LARGE` (the file is larger than 20971520 bytes; `error.details.limit_bytes` says the cap, and no byte is sent).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate limit is exhausted. `Retry-After` says when to come back and `error.details.scope` which limit it was: `key`, `tenant` or `service`; with a portfolio key also `portfolio_key`, `portfolio_org` or `portfolio_tenant`.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window that is closest to being exhausted.","schema":{"type":"integer","minimum":1}},"RateLimit-Remaining":{"description":"Requests left in that window.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until that window renews.","schema":{"type":"integer","minimum":0}},"RateLimit-Policy":{"description":"`;w=`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Something failed on our side. `error.request_id` is what support needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The service cannot verify credentials right now. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]},{"oauth2":["reports:read"]},{"portfolioKey":[]}],"x-hs-scope":{"scopes":["reports:read"]}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"additionalProperties":false,"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","enum":["invalid_request","unauthorized","forbidden","not_found","method_not_allowed","conflict","rate_limited","internal_error","service_unavailable"],"description":"Closed vocabulary; new members may be added (see Versioning)."},"message":{"type":"string","description":"For a human reading a log. Not stable; do not parse it."},"request_id":{"type":"string","description":"Also the `X-Request-Id` response header."},"details":{"type":"object","additionalProperties":true,"description":"Diagnostic, except `code` and the keys the operation’s error descriptions name, which are contract.","properties":{"code":{"type":"string","description":"OPEN vocabulary (a value keeps its meaning and its HTTP status; new values may be added): `TENANT_HEADER_INVALID`, `TENANT_REQUIRED`, `TENANT_SELECTOR_NOT_SUPPORTED` (400), `INSUFFICIENT_SCOPE`, `LICENCE_INACTIVE` (403), `TENANT_NOT_FOUND` (404), `REPORT_NOT_READY`, `FORMAT_UNSUPPORTED`, `FILE_TOO_LARGE` (409).","x-hs-known-values":["TENANT_HEADER_INVALID","TENANT_REQUIRED","TENANT_SELECTOR_NOT_SUPPORTED","INSUFFICIENT_SCOPE","LICENCE_INACTIVE","TENANT_NOT_FOUND","REPORT_NOT_READY","FORMAT_UNSUPPORTED","FILE_TOO_LARGE"]}}}}}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A tenant API key, created in the console under Settings → API keys with the minimum scopes it needs."},"portfolioKey":{"type":"http","scheme":"bearer","description":"An MSSP portfolio key (`hsp_…`), created by an organisation administrator of an active managed security service provider in the console’s MSSP area (API keys). It lists the provider’s client tenants (`portfolio:read`) and reads ONE of them per request, selected with `X-HS-Tenant`, with the same data scopes a tenant API key uses. Read-only; mandatory expiry (default 90 days, at most 365); optional client subset and source IP ranges. Refused with 401 everywhere but `/v1`."},"oauth2":{"type":"oauth2","description":"A USER access token (`hs_at_…`) issued by the HodeiShield console, the OAuth 2.1 authorization server at `https://app.hodeishield.com/api/auth` (metadata: `/.well-known/oauth-authorization-server` on its origin). Authorization code with PKCE (S256, mandatory) for applications with a browser; the RFC 8628 device authorization grant (`https://app.hodeishield.com/api/auth/device/code`) for those without one. Every token is bound to ONE user and ONE tenant, chosen by the user on the consent screen, and carries read scopes only; the tenant is re-checked against what the user can see on every request. Public clients register themselves (RFC 7591) at `/oauth2/register`; the HodeiShield CLI is pre-registered as `hodeishield-cli`. Revocation: RFC 7009 at `/oauth2/revoke`, or by the user in the console under Account → Application access.","flows":{"authorizationCode":{"authorizationUrl":"https://app.hodeishield.com/api/auth/oauth2/authorize","tokenUrl":"https://app.hodeishield.com/api/auth/oauth2/token","refreshUrl":"https://app.hodeishield.com/api/auth/oauth2/token","scopes":{"supply_risk:read":"Read supply_risk data of the tenant the token is bound to.","compliance.nis2:read":"Read compliance.nis2 data of the tenant the token is bound to.","compliance.ens:read":"Read compliance.ens data of the tenant the token is bound to.","compliance.iso27001:read":"Read compliance.iso27001 data of the tenant the token is bound to.","compliance.dora:read":"Read compliance.dora data of the tenant the token is bound to.","compliance.gdpr:read":"Read compliance.gdpr data of the tenant the token is bound to.","compliance.soc2:read":"Read compliance.soc2 data of the tenant the token is bound to.","compliance.iso42001:read":"Read compliance.iso42001 data of the tenant the token is bound to.","compliance.nistcsf:read":"Read compliance.nistcsf data of the tenant the token is bound to.","compliance.pcidss:read":"Read compliance.pcidss data of the tenant the token is bound to.","compliance.euaiact:read":"Read compliance.euaiact data of the tenant the token is bound to.","compliance.hipaa:read":"Read compliance.hipaa data of the tenant the token is bound to.","compliance.cmmc:read":"Read compliance.cmmc data of the tenant the token is bound to.","compliance.cyberessentials:read":"Read compliance.cyberessentials data of the tenant the token is bound to.","compliance.uaeia:read":"Read compliance.uaeia data of the tenant the token is bound to.","compliance.sama:read":"Read compliance.sama data of the tenant the token is bound to.","compliance.saudiecc:read":"Read compliance.saudiecc data of the tenant the token is bound to.","compliance.ndpa:read":"Read compliance.ndpa data of the tenant the token is bound to.","radar:read":"Read radar data of the tenant the token is bound to.","connectors:read":"Read connectors data of the tenant the token is bound to.","endpoints:read":"Read endpoints data of the tenant the token is bound to.","hodeiai:read":"Read hodeiai data of the tenant the token is bound to.","virtual_ciso:read":"Read virtual_ciso data of the tenant the token is bound to.","simulator:read":"Read simulator data of the tenant the token is bound to.","evidence:read":"Read evidence data of the tenant the token is bound to.","reports:read":"Read reports data of the tenant the token is bound to.","ai_governance:read":"Read ai_governance data of the tenant the token is bound to.","management:read":"Read management data of the tenant the token is bound to.","offline_access":"Issue a refresh token (30 days, rotated on every use)."}}}}}},"security":[{"bearerAuth":[]},{"oauth2":[]},{"portfolioKey":[]}]} \ No newline at end of file diff --git a/xtask/src/main.rs b/xtask/src/main.rs index d7359ee..0b61dfa 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -32,7 +32,7 @@ use std::process::{Command, ExitCode, Stdio}; const SPEC: &str = "openapi/v1.json"; const OUT: &str = "crates/hodeishield-api/src/generated.rs"; /// SHA-256 of `openapi/v1.json`. Updating the document means updating this and `openapi/README.md`. -const EXPECTED_SHA256: &str = "8c9a44bc696584105f10b67d490a8816dabc26b07b80ed18369e6d3e498359c4"; +const EXPECTED_SHA256: &str = "26e5a79d4669eaa3394c9917e66d56af546fc861d6576af1d6caada37c9c3458"; /// Type names that the default naming rules would get wrong, keyed by the schema's location. const RENAMES: &[(&str, &str)] = &[ @@ -43,6 +43,10 @@ const RENAMES: &[(&str, &str)] = &[ "WhoAmIOrganization", ), ("getReport/responses/200/properties/data", "ReportDetail"), + ( + "listAlerts/responses/200/properties/pagination", + "AlertsPagination", + ), ]; type Result = std::result::Result;