{
  "openapi": "3.1.0",
  "info": {
    "title": "External API",
    "description": "\nCustomer-facing API for the MySecutec platform. Poll your organization's\nthreats, assets and DNS data through a stable, versioned contract.\n\n### Authentication\n\nOAuth2 **client credentials** only. Request a token from the platform's Keycloak\ntoken endpoint with the client id and secret issued for your organization, and\nsend it as `Authorization: Bearer <token>`.\n\nAccess tokens are valid for 5 minutes and no refresh token is issued — when one\nexpires, request another with the same credentials.\n\n### Pagination\n\nList endpoints are cursor-paginated and return items ordered by `updated_at`\n(`blocked_at` for DNS blocks, which are immutable events).\nPass `limit` (default 100, max 500) and, from the second page onward, the\n`next_cursor` value from the previous response. Treat the cursor as opaque.\n\nKeep polling until `next_cursor` is `null` — that is the end of the stream. To\nresume later, re-use the last non-null cursor you received. Delivery is\nat-least-once, so de-duplicate on the resource `id`.\n\n### Errors\n\nEvery error is [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem\nDetails with content type `application/problem+json`. Match on the `type` URI,\nwhich is stable across releases; `title` and `status` are informational and\n`detail` describes the individual occurrence.\n\nResponses carry an `X-Request-ID` header. Quote it in a support request.\n\n### Versioning\n\nThe version is in the path (`/v1`). Additive changes — a new optional field,\nquery parameter, endpoint or error code — ship on `/v1` without notice, so\nignore fields you do not recognise. Anything that could break a working client\ngets a new major version, and `/v1` keeps serving.\n",
    "version": "0.1.0"
  },
  "servers": [
    {
      "url": "https://api.secutec.com"
    }
  ],
  "paths": {
    "/v1/assets": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "List assets by delta",
        "description": "Return the organization's assets changed since the cursor, oldest first.\n\nItems arrive in a stable, monotonic order — this is the only ordering the\nendpoint offers and there is no `sort` parameter. **That order is not the\n`updated_at` field on the resource**: an asset takes its place in the stream\nby when the platform finished processing the change, which can be slightly\nlater. A page may therefore contain an asset whose `updated_at` is older\nthan one you saw earlier; sort by `updated_at` yourself if you need it.\n\nDelivery is at-least-once — an asset updated while you poll may appear again\non a later page, so de-duplicate on `id`. A page shorter than your `limit`\nis not the end of the stream; only `next_cursor: null` is. Keep polling\nuntil you get it, then poll again later with the last non-null cursor you\nreceived to pick up new changes.\n\n**Removals arrive as tombstones.** When an asset is removed you receive it\nonce more with `deleted: true` and `deleted_at` set — a removal is a change\nlike any other, so the stream delivers it like any other. Drop the asset\nfrom your copy when you see one. A removed asset that is later restored\narrives again with `deleted: false`.\n\n**A tombstone also ends that asset's threats.** Threats belong to exactly\none asset, so when you receive an asset tombstone, drop that asset's threats\ntoo — `GET /v1/threats` will not report them separately.\n\nYou may receive a tombstone for an asset you never saw, if it was created\nand removed between two of your polls. Ignore it.",
        "operationId": "list_assets_v1_assets_get",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Opaque cursor from the previous response's `next_cursor`. Omit it on the first poll; treat the value as a black box.",
              "title": "Cursor"
            },
            "description": "Opaque cursor from the previous response's `next_cursor`. Omit it on the first poll; treat the value as a black box."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 500,
              "minimum": 1,
              "description": "Page size: default 100, minimum 1, maximum 500.",
              "default": 100,
              "title": "Limit"
            },
            "description": "Page size: default 100, minimum 1, maximum 500."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetsResponseDTO"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-cursor",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "The cursor is not valid for this endpoint."
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/path/not-allowlisted",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "Unknown endpoint."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{id}": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "Get one asset",
        "description": "Return one asset by `id` — the same schema as the list items, as a bare object.\n\nRemoved (soft-deleted) assets are returned too, flagged with `deleted`\nand `deleted_at` — the same shape `GET /v1/assets` delivers them as when\nthey are removed. `404 resource/not-found` when no asset with this id\nexists in your organization.",
        "operationId": "get_asset_v1_assets__id__get",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetDTO"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-filter",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "The id in the path is not a valid UUID."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/resource/not-found",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "No resource with that id exists for your organization."
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/related": {
      "get": {
        "tags": [
          "Assets"
        ],
        "summary": "List an asset's related assets",
        "description": "Return the assets linked to `{asset_id}`, oldest change first.\n\nRelations are **symmetric**: an asset linked to `{asset_id}` is returned\nwhichever side of the link it sits on. Typical links are an M365 tenant to\nits accounts, or a domain to the IPs it resolves to.\n\nEnvelope, ordering, cursor and `limit` are identical to `GET /v1/assets` —\nthe same stable, monotonic order (not `updated_at`; see that endpoint) and\nat-least-once delivery, so de-duplicate on `id` and keep polling until\n`next_cursor` is `null`, a short page notwithstanding. `{asset_id}` itself\nis never included.\n\n**Removed assets are not listed here**, and this endpoint emits no\ntombstones — unlike `GET /v1/assets`. This is a view of the asset's current\nneighbourhood, answered on demand; a removed neighbour simply stops\nappearing. Track removals on the asset stream.\n\nAn asset that exists but has no relations returns an empty `assets` array.\nAn `{asset_id}` your organization has no asset for returns **404**, checked\non the first (cursor-less) poll only — a request carrying a `cursor` is\ntreated as mid-stream and answers with an empty page instead. So a mistyped\nid shows up as a 404 on your first call, not on a resumed one.",
        "operationId": "list_related_assets_v1_assets__asset_id__related_get",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Identifier of the asset whose related assets you want, as returned in an asset's `id`.",
              "title": "Asset Id"
            },
            "description": "Identifier of the asset whose related assets you want, as returned in an asset's `id`."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Opaque cursor from the previous response's `next_cursor`. Omit it on the first poll; treat the value as a black box.",
              "title": "Cursor"
            },
            "description": "Opaque cursor from the previous response's `next_cursor`. Omit it on the first poll; treat the value as a black box."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 500,
              "minimum": 1,
              "description": "Page size: default 100, minimum 1, maximum 500.",
              "default": 100,
              "title": "Limit"
            },
            "description": "Page size: default 100, minimum 1, maximum 500."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetsResponseDTO"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-cursor",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "The cursor is not valid for this endpoint."
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/resource/not-found",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "No resource with that id exists for your organization."
                }
              }
            }
          }
        }
      }
    },
    "/v1/threats": {
      "get": {
        "tags": [
          "Threats"
        ],
        "summary": "List threats by delta",
        "description": "Return the organization's threats changed since the cursor, oldest first.\n\nItems arrive in a stable, monotonic order — this is the only ordering the\nendpoint offers and there is no `sort` parameter. **That order is not the\n`updated_at` field on the resource**: a threat takes its place in the stream\nby when the platform finished processing the change, which can be slightly\nlater. A page may therefore contain a threat whose `updated_at` is older\nthan one you saw earlier; sort by `updated_at` yourself if you need it.\n\nDelivery is at-least-once — a threat updated while you poll may appear again\non a later page, so de-duplicate on `id`. A page shorter than your `limit`\nis not the end of the stream; only `next_cursor: null` is. Keep polling\nuntil you get it, then poll again later\nwith the last non-null cursor you received to pick up new changes.\n`module_data` carries per-module enrichment — populated for `lcm` threats\nin v1, `null` for other modules. It is also `null` for an `lcm` threat that\nsimply has no enrichment on record: an object whose every field is null is\nnever returned, so a non-null `module_data` always carries at least one\nvalue.",
        "operationId": "list_threats_v1_threats_get",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Opaque cursor from the previous response's `next_cursor`. Omit it on the first poll; treat the value as a black box.",
              "title": "Cursor"
            },
            "description": "Opaque cursor from the previous response's `next_cursor`. Omit it on the first poll; treat the value as a black box."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 500,
              "minimum": 1,
              "description": "Page size: default 100, minimum 1, maximum 500.",
              "default": 100,
              "title": "Limit"
            },
            "description": "Page size: default 100, minimum 1, maximum 500."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThreatsResponseDTO"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-cursor",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "The cursor is not valid for this endpoint."
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/path/not-allowlisted",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "Unknown endpoint."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          }
        }
      }
    },
    "/v1/threats/{threat_id}": {
      "get": {
        "tags": [
          "Threats"
        ],
        "summary": "Get one threat",
        "description": "Return one threat by its identifier.\n\nThe object is returned **bare** — no envelope and no `pagination` block,\nunlike `GET /v1/threats`. Every field is the one the list endpoint gives you\nfor the same threat, so anything built against the list reads this response\nunchanged; `module_data` carries per-module enrichment, populated for `lcm`\nthreats in v1 and `null` for other modules — and equally `null` for an `lcm`\nthreat with no enrichment on record, exactly as on the list.\n\nA `{threat_id}` your organization has no threat for returns **404**. So does\na threat attached to an asset that has since been deleted — deleted assets\ntake their threats out of the API with them, exactly as in `GET /v1/threats`.",
        "operationId": "get_threat_v1_threats__threat_id__get",
        "parameters": [
          {
            "name": "threat_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Identifier of the threat you want, as returned in a threat's `id`.",
              "title": "Threat Id"
            },
            "description": "Identifier of the threat you want, as returned in a threat's `id`."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThreatDTO"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-filter",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "The id in the path is not a valid UUID."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/resource/not-found",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "No resource with that id exists for your organization."
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Threats"
        ],
        "summary": "Update a threat's status",
        "description": "Record your verdict on a threat and return the full updated threat.\n\nYou are setting *your organization's verdict* — `unsolved`, `solved`,\n`false_positive` or `accepted`, the same actions MySecutec offers.\n`accepted` requires a non-empty `status_note`; `pending` cannot be set\n(400 `request/invalid-status-transition`) — it is computed by the platform.\n\nThe response is the full threat, **bare**, identical to\n`GET /v1/threats/{threat_id}` — no follow-up read needed. Read the\n`status` it carries rather than assuming your input: the effective status\nis recalculated and can differ from the verdict you set. On findings the\nplatform verifies automatically, `solved` is a claim of remediation and\ncomes back `pending` until the next scan confirms it.\n\nThe gateway sends your update upstream exactly once — it is never retried\nautomatically. If the request times out, the outcome is unknown; re-sending\nis safe and reaches the same end state, but re-stamps the status-change\ntimestamp, so the threat is delivered again on the `GET /v1/threats` delta\nstream. A successful update also bumps `updated_at` — expect the threat on\nyour next delta poll.",
        "operationId": "update_threat_status_v1_threats__threat_id__patch",
        "parameters": [
          {
            "name": "threat_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Identifier of the threat to update, as returned in a threat's `id`.",
              "title": "Threat Id"
            },
            "description": "Identifier of the threat to update, as returned in a threat's `id`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateThreatStatusRequestDTO"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThreatDTO"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/validation",
                  "title": "Validation Error",
                  "status": 422,
                  "detail": "Request body is invalid."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-status-transition",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "This status cannot be set. Settable statuses: unsolved, solved, false_positive, accepted."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/resource/not-found",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "No resource with that id exists for your organization."
                }
              }
            }
          }
        }
      }
    },
    "/v1/threats/{threat_id}/update-note": {
      "post": {
        "tags": [
          "Threats"
        ],
        "summary": "Update a threat's note",
        "description": "Replace the free-form note on a threat.\n\nWrites the conclusion your SOAR or analyst reached onto the threat so it is\nvisible to everyone working it in MySecutec. The body carries the new\n`note`, which **replaces** the current one; only the note changes — the\nthreat's `status` is untouched, and this operation cannot set it.\n\nThe response is **202 Accepted with an empty body**. The note is applied\nasynchronously: `GET /v1/threats/{threat_id}` immediately afterwards may\nstill return the previous note for a short moment. A successful write bumps\nthe threat's `updated_at`, so expect it on your next `GET /v1/threats`\ndelta poll.\n\nA `{threat_id}` your organization has no threat for returns **404**, and\nnothing is written — the same answer whether the threat does not exist or\nbelongs to someone else.\n\nThe gateway sends your note upstream exactly once — it is never retried\nautomatically. If the request times out, the outcome is unknown; re-sending\nthe same note is safe and reaches the same end state.",
        "operationId": "update_threat_note_v1_threats__threat_id__update_note_post",
        "parameters": [
          {
            "name": "threat_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Identifier of the threat to annotate, as returned in a threat's `id`.",
              "title": "Threat Id"
            },
            "description": "Identifier of the threat to annotate, as returned in a threat's `id`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateThreatNoteRequestDTO"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted — the note is being applied. The body is empty."
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/validation",
                  "title": "Validation Error",
                  "status": 422,
                  "detail": "Request body is invalid."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-filter",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "The id in the path is not a valid UUID."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/resource/not-found",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "No resource with that id exists for your organization."
                }
              }
            }
          }
        }
      }
    },
    "/v1/threats/{threat_id}/external-references/{system}": {
      "put": {
        "tags": [
          "Threats"
        ],
        "summary": "Link a threat to a record in your own system",
        "description": "Record your own incident or ticket id against a threat.\n\nCloses the loop between MySecutec and your SOAR. Without it the link runs one way — your\nincident knows its SecureSight `id` and SecureSight knows nothing about your incident — so\n\"is this already being worked?\" is a lookup in the other system every time. The reference\ncomes back on `GET /v1/threats` and `GET /v1/threats/{threat_id}` as `external_references`,\nso your poller can answer it from the row it already has.\n\n**Safe to retry.** `system` is the key: sending the same one twice replaces the link rather\nthan adding a second, so a playbook that retries after a timeout cannot create duplicates.\nYou do not need an idempotency key, and there is none to send. Sending a *different*\n`external_id` for the same `system` is how you move the link.\n\nOne reference per `system`; use different `system` values to link the same threat into\nseveral of your tools at once.\n\n`linked_at` is set by the platform, not by you.\n\nA successful write bumps the threat's `updated_at`, so expect the threat on your next\n`GET /v1/threats` delta poll. The reference reaches that stream asynchronously, so an\nimmediate read-back may briefly show the previous state.\n\nA `{threat_id}` your organization has no threat for returns **404**, and nothing is written.",
        "operationId": "upsert_external_reference_v1_threats__threat_id__external_references__system__put",
        "parameters": [
          {
            "name": "threat_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Identifier of the threat to link, as returned in a threat's `id`.",
              "title": "Threat Id"
            },
            "description": "Identifier of the threat to link, as returned in a threat's `id`."
          },
          {
            "name": "system",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64,
              "description": "Your own name for the system holding the record — for example `xsoar`, `servicenow` or `jira`. This is the **key**: writing the same `system` again replaces the link rather than adding a second one.",
              "examples": [
                "xsoar"
              ],
              "title": "System"
            },
            "description": "Your own name for the system holding the record — for example `xsoar`, `servicenow` or `jira`. This is the **key**: writing the same `system` again replaces the link rather than adding a second one."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertExternalReferenceRequestDTO"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "The link is recorded. The body is empty."
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/validation",
                  "title": "Validation Error",
                  "status": 422,
                  "detail": "Request body is invalid."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-filter",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "The id in the path is not a valid UUID."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/resource/not-found",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "No resource with that id exists for your organization."
                }
              }
            }
          }
        }
      }
    },
    "/v1/sdns/blocks": {
      "get": {
        "tags": [
          "Secure DNS"
        ],
        "summary": "List blocked DNS queries by delta",
        "description": "Return the organization's blocked DNS queries, oldest first.\n\nFixed order `blocked_at ASC, id ASC`. A block is an event that never\nchanges, so `blocked_at` is what `updated_at` is for threats and assets.\nKeep polling until `next_cursor` is `null`, then poll again later with the\nlast non-null cursor you received to pick up new blocks. Delivery is\nat-least-once — de-duplicate on `id`.\n\n`start` and `end` bound `blocked_at` (inclusive); without `start` the\nstream begins 30 days back. The most recent 60 seconds are withheld so a\nrecord still being indexed is never skipped by a delta poll — expect a\nblock about a minute after it happened. A record indexed later than that\nwill not appear on a poll whose cursor has already passed its\n`blocked_at`; re-read the window with `start`/`end` if you need to\nreconcile.\n\nFilters (`categories`, `sites`, `source_devices`, `internal_ips`,\n`domains`) take repeated keys: several values for one filter match any of\nthem, several filters must all match. Send the same filters on every page\nof a stream — the cursor encodes only the position, not the filters.",
        "operationId": "list_blocks_v1_sdns_blocks_get",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Opaque cursor from the previous response's `next_cursor`. Omit it on the first poll; treat the value as a black box.",
              "title": "Cursor"
            },
            "description": "Opaque cursor from the previous response's `next_cursor`. Omit it on the first poll; treat the value as a black box."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 500,
              "minimum": 1,
              "description": "Page size: default 100, minimum 1, maximum 500.",
              "default": 100,
              "title": "Limit"
            },
            "description": "Page size: default 100, minimum 1, maximum 500."
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date-time"
                },
                {
                  "type": "null"
                }
              ],
              "description": "ISO 8601 datetime **with a UTC offset** (e.g. `2026-09-01T00:00:00Z`). Return blocks with `blocked_at` at or after this instant. Defaults to 30 days before the request. Must be before `end` and not in the future.",
              "title": "Start"
            },
            "description": "ISO 8601 datetime **with a UTC offset** (e.g. `2026-09-01T00:00:00Z`). Return blocks with `blocked_at` at or after this instant. Defaults to 30 days before the request. Must be before `end` and not in the future."
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date-time"
                },
                {
                  "type": "null"
                }
              ],
              "description": "ISO 8601 datetime **with a UTC offset**. Return blocks with `blocked_at` at or before this instant. Defaults to the request time. The most recent 60 seconds are always withheld so records still being indexed are not skipped by a delta poll.",
              "title": "End"
            },
            "description": "ISO 8601 datetime **with a UTC offset**. Return blocks with `blocked_at` at or before this instant. Defaults to the request time. The most recent 60 seconds are always withheld so records still being indexed are not skipped by a delta poll."
          },
          {
            "name": "categories",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 253
                  },
                  "maxItems": 50
                },
                {
                  "type": "null"
                }
              ],
              "description": "Security categories to include, e.g. `botnet`, `malware`, `phishing`. Repeat the key for several values (`categories=botnet&categories=phishing`).",
              "title": "Categories"
            },
            "description": "Security categories to include, e.g. `botnet`, `malware`, `phishing`. Repeat the key for several values (`categories=botnet&categories=phishing`)."
          },
          {
            "name": "sites",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 253
                  },
                  "maxItems": 50
                },
                {
                  "type": "null"
                }
              ],
              "description": "Customer sites to include. Repeat the key for several values.",
              "title": "Sites"
            },
            "description": "Customer sites to include. Repeat the key for several values."
          },
          {
            "name": "source_devices",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 253
                  },
                  "maxItems": 50
                },
                {
                  "type": "null"
                }
              ],
              "description": "Device or agent names to include. Repeat the key for several values.",
              "title": "Source Devices"
            },
            "description": "Device or agent names to include. Repeat the key for several values."
          },
          {
            "name": "internal_ips",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 253
                  },
                  "maxItems": 50
                },
                {
                  "type": "null"
                }
              ],
              "description": "Internal (private) IP addresses to include. Repeat the key for several values.",
              "title": "Internal Ips"
            },
            "description": "Internal (private) IP addresses to include. Repeat the key for several values."
          },
          {
            "name": "domains",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 253
                  },
                  "maxItems": 50
                },
                {
                  "type": "null"
                }
              ],
              "description": "Blocked domain names to include. Repeat the key for several values.",
              "title": "Domains"
            },
            "description": "Blocked domain names to include. Repeat the key for several values."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlocksResponseDTO"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/request/invalid-cursor",
                  "title": "Bad Request",
                  "status": 400,
                  "detail": "The cursor is not valid for this endpoint."
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/missing-token",
                  "title": "Unauthorized",
                  "status": 401,
                  "detail": "Authorization header is missing."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/auth/insufficient-role",
                  "title": "Forbidden",
                  "status": 403,
                  "detail": "Token lacks the required role."
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/path/not-allowlisted",
                  "title": "Not Found",
                  "status": 404,
                  "detail": "Unknown endpoint."
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/rate-limit/exceeded",
                  "title": "Too Many Requests",
                  "status": 429,
                  "detail": "Rate limit exceeded."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Whole seconds to wait before retrying. Always present on a 429.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            }
          },
          "500": {
            "description": "Server Error",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetailDTO"
                },
                "example": {
                  "type": "https://api.secutec.com/errors/server/internal-error",
                  "title": "Server Error",
                  "status": 500,
                  "detail": "An unexpected error occurred."
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AssetDTO": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id",
            "description": "Stable customer-facing identifier. Use for de-duplication and threat→asset joins."
          },
          "type": {
            "type": "string",
            "title": "Type",
            "description": "Asset type — see the Asset Types reference. Examples: `domain`, `email`, `ip`, `account`, `tenant`. Treated as a stable enum; new values are additive on /v1."
          },
          "name": {
            "type": "string",
            "title": "Name",
            "description": "Human-readable display name."
          },
          "identifying_properties": {
            "additionalProperties": true,
            "type": "object",
            "title": "Identifying Properties",
            "description": "Type-specific identifying fields; the shape depends on `type`. Examples: `{\"email\": \"...\"}` for an email asset, `{\"tenant_id\": \"...\"}` for an M365 tenant."
          },
          "public_facing": {
            "type": "boolean",
            "title": "Public Facing",
            "description": "True if the asset is internet-exposed."
          },
          "business_value": {
            "type": "integer",
            "maximum": 5.0,
            "minimum": 1.0,
            "title": "Business Value",
            "description": "Customer-editable business-value rating (1 = lowest, 5 = highest). Defaults to 2 when unset."
          },
          "note": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Note",
            "description": "Free-form customer-editable note attached to the asset."
          },
          "version": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Version",
            "description": "Optional version string, typically populated for software-type assets; null for most other types."
          },
          "deleted": {
            "type": "boolean",
            "title": "Deleted",
            "description": "True if the asset has been removed. `GET /v1/assets` delivers a removed asset once with this set — a tombstone — so drop it, and its threats, from your copy. `GET /v1/assets/{id}` returns removed assets flagged the same way. The one exception is `GET /v1/assets/{id}/related`, which lists only active neighbours."
          },
          "deleted_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Deleted At",
            "description": "ISO 8601 UTC — when the asset was removed; null while it is active."
          },
          "modules": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Modules",
            "description": "Modules that currently track this asset, derived from its underlying tech-solutions. An asset can be tracked by multiple modules."
          },
          "first_seen_at": {
            "type": "string",
            "format": "date-time",
            "title": "First Seen At",
            "description": "ISO 8601 UTC — the moment the platform first stored a record for this asset."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Updated At",
            "description": "ISO 8601 UTC — when the resource itself last changed. This is NOT the pagination key; see `stream_position`."
          },
          "stream_position": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Stream Position",
            "description": "ISO 8601 UTC — when this version of the resource became available on the delta stream. This is the stream's ordering key: it is non-decreasing across a cursor walk and is the field to window on when deciding what is new **to you**. `updated_at` answers a different question — when the resource itself last changed — and a resource can be re-delivered at a later `stream_position` with its `updated_at` unmoved. Null on single-resource reads (`GET /v1/{resource}/{id}`), which return a resource rather than a position in a stream. Not a business timestamp: it moves whenever the platform re-projects the row, including for reasons you did not cause."
          }
        },
        "type": "object",
        "required": [
          "id",
          "type",
          "name",
          "identifying_properties",
          "public_facing",
          "business_value",
          "note",
          "version",
          "deleted",
          "deleted_at",
          "modules",
          "first_seen_at",
          "updated_at"
        ],
        "title": "AssetDTO",
        "description": "One asset, projected for customers."
      },
      "AssetSummaryDTO": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id",
            "description": "Asset identifier — joins to the `id` field of `GET /v1/assets`."
          },
          "name": {
            "type": "string",
            "title": "Name",
            "description": "Asset display name."
          },
          "type": {
            "type": "string",
            "title": "Type",
            "description": "Asset type — see the Asset Types reference. Examples: `domain`, `email`, `ip`."
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "type"
        ],
        "title": "AssetSummaryDTO",
        "description": "The minimal embedded asset (BR §3.3 ``AssetSummary``)."
      },
      "AssetsResponseDTO": {
        "properties": {
          "assets": {
            "items": {
              "$ref": "#/components/schemas/AssetDTO"
            },
            "type": "array",
            "title": "Assets",
            "description": "Assets in the stream's fixed order, oldest change first. Not `updated_at` order — see the endpoint description."
          },
          "pagination": {
            "$ref": "#/components/schemas/PaginationDTO"
          }
        },
        "type": "object",
        "required": [
          "assets",
          "pagination"
        ],
        "title": "AssetsResponseDTO",
        "description": "The list envelope: a resource-named array next to the pagination block."
      },
      "BlockDTO": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id",
            "description": "Stable identifier of the blocked query. Use it for de-duplication across polls."
          },
          "blocked_at": {
            "type": "string",
            "format": "date-time",
            "title": "Blocked At",
            "description": "ISO 8601 UTC — when the DNS query was blocked (event time, not ingestion time). Drives the pagination cursor."
          },
          "domain": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Domain",
            "description": "Fully qualified domain name that was requested and blocked."
          },
          "parent_domain": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Parent Domain",
            "description": "Registrable parent domain of `domain`, when the platform derived one."
          },
          "category": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Category",
            "description": "Security category that triggered the block, e.g. `botnet`, `malware`, `phishing`. Treated as a stable enum; new values are additive on /v1."
          },
          "site": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Site",
            "description": "Customer site the query originated from."
          },
          "public_ip": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Public Ip",
            "description": "Public IP address the query reached the resolver from."
          },
          "internal_ips": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Internal Ips",
            "description": "Internal (private) IP addresses of the requesting host, when known."
          },
          "source_devices": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Source Devices",
            "description": "Names of the devices or agents that issued the query, when known."
          },
          "source_device_type": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "agent",
                  "server"
                ]
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Device Type",
            "description": "`agent` for a managed Secure DNS agent on a device, `server` for a site resolver."
          }
        },
        "type": "object",
        "required": [
          "id",
          "blocked_at",
          "domain",
          "parent_domain",
          "category",
          "site",
          "public_ip",
          "internal_ips",
          "source_devices",
          "source_device_type"
        ],
        "title": "BlockDTO",
        "description": "One blocked DNS query, projected for customers."
      },
      "BlocksResponseDTO": {
        "properties": {
          "blocks": {
            "items": {
              "$ref": "#/components/schemas/BlockDTO"
            },
            "type": "array",
            "title": "Blocks",
            "description": "Blocked queries in fixed `blocked_at ASC, id ASC` order."
          },
          "pagination": {
            "$ref": "#/components/schemas/PaginationDTO"
          }
        },
        "type": "object",
        "required": [
          "blocks",
          "pagination"
        ],
        "title": "BlocksResponseDTO",
        "description": "The list envelope: a resource-named array next to the pagination block."
      },
      "ExternalReferenceDTO": {
        "properties": {
          "system": {
            "type": "string",
            "title": "System",
            "description": "Your own name for the system, e.g. `xsoar`, `servicenow`, `jira`."
          },
          "external_id": {
            "type": "string",
            "title": "External Id",
            "description": "The record's identifier in that system."
          },
          "url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Url",
            "description": "A deep link to the record; null when you did not supply one."
          },
          "linked_at": {
            "type": "string",
            "format": "date-time",
            "title": "Linked At",
            "description": "ISO 8601 UTC — when the link was written. Set by the platform, not by you: a caller-supplied timestamp would be the only field here that could disagree with what actually happened."
          }
        },
        "type": "object",
        "required": [
          "system",
          "external_id",
          "linked_at"
        ],
        "title": "ExternalReferenceDTO",
        "description": "A link from this threat to a record in a system you operate."
      },
      "LcmModuleDataDTO": {
        "properties": {
          "module": {
            "type": "string",
            "const": "lcm",
            "title": "Module",
            "description": "Discriminator; always matches the threat's `module` value.",
            "default": "lcm"
          },
          "breach_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Breach Name",
            "description": "Name of the breach or leak corpus the credential was found in — for infostealer records this names the stealer family (e.g. `RedLine Stealer`)."
          },
          "breach_category": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Breach Category",
            "description": "Leak classification, e.g. `infostealer`, `combolist` or `breach`."
          },
          "source_url": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Source Url",
            "description": "URL the compromised credential grants access to, when known."
          },
          "credential_domain": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Credential Domain",
            "description": "Domain the compromised credential belongs to."
          },
          "compromised_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Compromised At",
            "description": "ISO 8601 — when the machine or credential was compromised, when the source recorded it."
          },
          "password_category": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Password Category",
            "description": "Form of the leaked secret, e.g. `plaintext`, `hashed_weak`, `hashed_strong`, `no_password`, `cookie`."
          },
          "credential_category": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Credential Category",
            "description": "Credential class, e.g. `employee_credential` or `consumer_credential`."
          }
        },
        "type": "object",
        "required": [
          "breach_name",
          "breach_category",
          "source_url",
          "credential_domain",
          "compromised_at",
          "password_category",
          "credential_category"
        ],
        "title": "LcmModuleDataDTO",
        "description": "LCM enrichment (S7.A1) — the typed projection of leaked-credential source data."
      },
      "PaginationDTO": {
        "properties": {
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Next Cursor",
            "description": "Opaque cursor to pass on the next request to fetch the next page. `null` when no more data is available — it is the ONLY end-of-stream signal, so poll again later with the last non-null cursor you received. A page shorter than your `limit` still carries a cursor and does not mean the stream has ended; recent changes are briefly withheld so they cannot be skipped."
          },
          "page_size": {
            "type": "integer",
            "title": "Page Size",
            "description": "Number of items returned in this page (informational; equals the array length)."
          }
        },
        "type": "object",
        "required": [
          "next_cursor",
          "page_size"
        ],
        "title": "PaginationDTO",
        "description": "Cursor pagination block, rendered next to the resource-named array."
      },
      "ThreatDTO": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "title": "Id",
            "description": "Stable customer-facing identifier. Use for de-duplication across polls."
          },
          "name": {
            "type": "string",
            "title": "Name",
            "description": "Human-readable threat title."
          },
          "description": {
            "type": "string",
            "title": "Description",
            "description": "What was found and why it matters."
          },
          "note": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Note",
            "description": "Free-form customer-editable note attached to the threat."
          },
          "status": {
            "type": "string",
            "enum": [
              "unsolved",
              "solved",
              "false_positive",
              "accepted",
              "pending"
            ],
            "title": "Status",
            "description": "Calculated customer-facing status — the single effective value; the platform's internal system/user status split is not exposed."
          },
          "severity": {
            "type": "string",
            "enum": [
              "critical",
              "high",
              "medium",
              "low",
              "info",
              "unknown"
            ],
            "title": "Severity",
            "description": "CVSS-derived severity bucket. `info` corresponds to a 0.0 score; `unknown` means the threat carries no CVSS score."
          },
          "cvss_score": {
            "anyOf": [
              {
                "type": "number",
                "maximum": 10.0,
                "minimum": 0.0
              },
              {
                "type": "null"
              }
            ],
            "title": "Cvss Score",
            "description": "CVSS base score (0–10); null when the source does not score the finding."
          },
          "cvss_vector": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cvss Vector",
            "description": "CVSS vector string; null when not scored."
          },
          "risk_priority_score": {
            "anyOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "title": "Risk Priority Score",
            "description": "Platform-calculated risk prioritisation score; higher means more urgent."
          },
          "module": {
            "type": "string",
            "title": "Module",
            "description": "Originating module — one of `lcm`, `dnm`, `m365`, `pdk`, `sdns`, `asm`, `xdr`. Treated as a stable enum; new values are additive on /v1."
          },
          "asset": {
            "$ref": "#/components/schemas/AssetSummaryDTO",
            "description": "The asset this threat is attached to. Always set — threats are 1:1 with an asset."
          },
          "module_data": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LcmModuleDataDTO"
              },
              {
                "type": "null"
              }
            ],
            "description": "Module-specific enrichment, typed per module. Populated for `lcm` threats in v1; null for other modules (their schemas ship in later, additive releases), and null for an `lcm` threat with no enrichment on record — an object of nulls is never returned."
          },
          "first_seen_at": {
            "type": "string",
            "format": "date-time",
            "title": "First Seen At",
            "description": "ISO 8601 UTC — the moment the platform first recorded this threat."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Updated At",
            "description": "ISO 8601 UTC — when the resource itself last changed. This is NOT the pagination key; see `stream_position`."
          },
          "external_references": {
            "items": {
              "$ref": "#/components/schemas/ExternalReferenceDTO"
            },
            "type": "array",
            "title": "External References",
            "description": "Your own links from this threat out to records in systems you operate — one per `system`. Written with `PUT /v1/threats/{threat_id}/external-references/{system}`; the platform never populates these itself. Empty for a threat you have not linked."
          },
          "stream_position": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Stream Position",
            "description": "ISO 8601 UTC — when this version of the resource became available on the delta stream. This is the stream's ordering key: it is non-decreasing across a cursor walk and is the field to window on when deciding what is new **to you**. `updated_at` answers a different question — when the resource itself last changed — and a resource can be re-delivered at a later `stream_position` with its `updated_at` unmoved. Null on single-resource reads (`GET /v1/{resource}/{id}`), which return a resource rather than a position in a stream. Not a business timestamp: it moves whenever the platform re-projects the row, including for reasons you did not cause."
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "note",
          "status",
          "severity",
          "cvss_score",
          "cvss_vector",
          "risk_priority_score",
          "module",
          "asset",
          "module_data",
          "first_seen_at",
          "updated_at"
        ],
        "title": "ThreatDTO",
        "description": "One threat, projected for customers."
      },
      "ThreatsResponseDTO": {
        "properties": {
          "threats": {
            "items": {
              "$ref": "#/components/schemas/ThreatDTO"
            },
            "type": "array",
            "title": "Threats",
            "description": "Threats in the stream's fixed order, oldest change first. Not `updated_at` order — see the endpoint description."
          },
          "pagination": {
            "$ref": "#/components/schemas/PaginationDTO"
          }
        },
        "type": "object",
        "required": [
          "threats",
          "pagination"
        ],
        "title": "ThreatsResponseDTO",
        "description": "The list envelope: a resource-named array next to the pagination block."
      },
      "UpdateThreatNoteRequestDTO": {
        "properties": {
          "note": {
            "type": "string",
            "title": "Note",
            "description": "The note to store on the threat, replacing the current one. Free text, no length limit; an empty string stores an empty note. This is the threat's `note` field — the one `GET /v1/threats/{threat_id}` returns — not the verdict's `status_note`."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "note"
        ],
        "title": "UpdateThreatNoteRequestDTO",
        "description": "Body of ``POST /v1/threats/{threat_id}/update-note`` — the customer's note (DEV-2786).\n\nA separate body for a separate operation: the 2 Sep 2026 refinement chose a\ncommand-style POST over the BR's \"``note`` on the PATCH\" so that a note\nwrite can never move a verdict and a verdict can never clobber a note. The\nsame ``extra=\"forbid\"`` applies — a ``status`` key sent here is dropped\nintent, not a no-op.\n\nNo length limit, by decision: MySecutec's own note field has none (TAM's\n``ThreatPatchRequestDTO.note`` carries no constraint) and the External API\nmirrors the portal rather than inventing a bound customers would hit only\nhere. Accepted for v1 and flagged on the ticket as worth revisiting."
      },
      "UpdateThreatStatusRequestDTO": {
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "unsolved",
              "solved",
              "false_positive",
              "accepted",
              "pending"
            ],
            "title": "Status",
            "description": "Your verdict on this threat. Settable values: `unsolved`, `solved`, `false_positive`, `accepted`. `pending` is computed by the platform and cannot be set — sending it returns 400 `request/invalid-status-transition`. The response's `status` is recalculated and may differ from the value you set (see the response description)."
          },
          "status_note": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Status Note",
            "description": "Optional note explaining the status change. **Required when `status` is `accepted`** — accepting a risk needs a reason. Not the threat's free-form `note` field; this note is attached to the verdict itself."
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "status"
        ],
        "title": "UpdateThreatStatusRequestDTO",
        "description": "Body of ``PATCH /v1/threats/{threat_id}`` — the customer's status verdict.\n\n``extra=\"forbid\"``: on a write, a silently ignored key is silently dropped\ncustomer intent. ``note`` in particular is rejected here on purpose — the\nfree-form note has its own operation (``POST …/update-note``, DEV-2786),\nand accepting-and-dropping it would turn a note write into a lost write.\nUnknown keys fail loudly as 422 instead.\n\nThe ``status`` schema deliberately lists the full five-value enum with\n``pending`` documented as unsettable, rather than a four-value enum: sending\n``pending`` must be the contract's 400 ``request/invalid-status-transition``\n(decided in the use case), not a 422 shape error."
      },
      "UpsertExternalReferenceRequestDTO": {
        "properties": {
          "external_id": {
            "type": "string",
            "maxLength": 255,
            "minLength": 1,
            "title": "External Id",
            "description": "The record's identifier in your system, e.g. an XSOAR incident id.",
            "examples": [
              "INC-42"
            ]
          },
          "url": {
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2048
              },
              {
                "type": "null"
              }
            ],
            "title": "Url",
            "description": "Optional deep link to the record, for an analyst to open.",
            "examples": [
              "https://xsoar.example.com/#/incident/42"
            ]
          }
        },
        "additionalProperties": false,
        "type": "object",
        "required": [
          "external_id"
        ],
        "title": "UpsertExternalReferenceRequestDTO",
        "description": "The body of ``PUT /v1/threats/{threat_id}/external-references/{system}``.\n\n``system`` is a path segment rather than a body field: it is the key the upsert is on, so\ncarrying it in the body as well would allow a request whose path and body disagree, and then\nan implicit rule about which one wins."
      },
      "ProblemDetailDTO": {
        "description": "RFC 9457 Problem Details response body.",
        "properties": {
          "type": {
            "description": "URI identifying the error class — stable across releases.",
            "title": "Type",
            "type": "string"
          },
          "title": {
            "description": "Short human-readable summary; constant per type.",
            "title": "Title",
            "type": "string"
          },
          "status": {
            "description": "HTTP status code; always matches the response status.",
            "title": "Status",
            "type": "integer"
          },
          "detail": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Explanation specific to this occurrence.",
            "title": "Detail"
          },
          "instance": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Request path this occurrence relates to.",
            "title": "Instance"
          },
          "errors": {
            "anyOf": [
              {
                "items": {
                  "$ref": "#/components/schemas/ValidationErrorItem"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Field-level validation failures. Present only on 422 responses.",
            "title": "Errors"
          }
        },
        "required": [
          "type",
          "title",
          "status"
        ],
        "title": "ProblemDetailDTO",
        "type": "object"
      },
      "ValidationErrorItem": {
        "description": "One field-level failure. Only ever present on a 422.",
        "properties": {
          "field": {
            "description": "Name of the offending field.",
            "title": "Field",
            "type": "string"
          },
          "message": {
            "description": "What is wrong with it, in plain language.",
            "title": "Message",
            "type": "string"
          }
        },
        "required": [
          "field",
          "message"
        ],
        "title": "ValidationErrorItem",
        "type": "object"
      }
    }
  }
}
