{
  "openapi": "3.0.0",
  "paths": {
    "/api/v1/whoami": {
      "get": {
        "operationId": "PublicApiController_whoami",
        "summary": "Confirm the API key and view its tenant/key context",
        "description": "Any valid key (no scope required).",
        "parameters": [],
        "responses": {
          "200": {
            "description": "The resolved tenant and the key that authenticated the call.",
            "content": {
              "application/json": {
                "schema": {
                  "example": {
                    "tenant": {
                      "id": "t-1",
                      "name": "Acme MSSP",
                      "slug": "acme",
                      "notificationEmail": "soc@acme.example"
                    },
                    "key": {
                      "id": "k-1",
                      "name": "CI pipeline",
                      "scopes": [
                        "urlsearch",
                        "filescan"
                      ],
                      "createdByEmail": "admin@acme.example",
                      "lastUsedAt": "2026-07-11T12:00:00.000Z",
                      "expiresAt": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Identity"
        ]
      }
    },
    "/api/v1/urlsearch": {
      "get": {
        "operationId": "PublicApiController_urlsearch",
        "summary": "URL / domain reputation lookup",
        "description": "Requires an API key with the `urlsearch` scope.",
        "parameters": [
          {
            "name": "value",
            "required": true,
            "in": "query",
            "schema": {
              "example": "http://malicious.example/x",
              "type": "string"
            }
          },
          {
            "name": "type",
            "required": false,
            "in": "query",
            "schema": {
              "enum": [
                "url",
                "domain"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fused, multi-source ROUNDBOX reputation report. Fans the indicator out to every provider relevant to the search type, aggregates them, and layers a Henrietta (H-AI) rationale. `sources` is the ROUNDBOX Labs attribution only — underlying providers/feeds are never surfaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupReportDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid type or value"
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: urlsearch"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Reputation"
        ]
      }
    },
    "/api/v1/ipsearch": {
      "get": {
        "operationId": "PublicApiController_ipsearch",
        "summary": "IP / ASN reputation lookup",
        "description": "Requires an API key with the `ipsearch` scope.",
        "parameters": [
          {
            "name": "value",
            "required": true,
            "in": "query",
            "schema": {
              "example": "203.0.113.10",
              "type": "string"
            }
          },
          {
            "name": "type",
            "required": false,
            "in": "query",
            "schema": {
              "enum": [
                "ip",
                "asn"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fused, multi-source ROUNDBOX reputation report. Fans the indicator out to every provider relevant to the search type, aggregates them, and layers a Henrietta (H-AI) rationale. `sources` is the ROUNDBOX Labs attribution only — underlying providers/feeds are never surfaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupReportDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid type or value"
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: ipsearch"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Reputation"
        ]
      }
    },
    "/api/v1/dnssearch": {
      "get": {
        "operationId": "PublicApiController_dnssearch",
        "summary": "DNS / domain reputation lookup",
        "description": "Requires an API key with the `dnssearch` scope.",
        "parameters": [
          {
            "name": "value",
            "required": true,
            "in": "query",
            "schema": {
              "example": "malicious.example",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fused, multi-source ROUNDBOX reputation report. Fans the indicator out to every provider relevant to the search type, aggregates them, and layers a Henrietta (H-AI) rationale. `sources` is the ROUNDBOX Labs attribution only — underlying providers/feeds are never surfaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupReportDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing value"
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: dnssearch"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Reputation"
        ]
      }
    },
    "/api/v1/darkweb": {
      "get": {
        "operationId": "PublicApiController_darkweb",
        "summary": "Dark-web exposure lookup for a verified domain",
        "description": "Requires an API key with the `darkweb` scope. The domain must be registered and ownership-verified (DNS TXT challenge) under Domains first.",
        "parameters": [
          {
            "name": "value",
            "required": true,
            "in": "query",
            "schema": {
              "example": "acme.example",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Safe / Breached / unavailable dark-web credential-exposure verdict for the domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DarkwebResultDto"
                }
              }
            }
          },
          "400": {
            "description": "value must be a domain (FQDN)"
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: darkweb — or the domain is not ownership-verified (`error: \"domain-not-verified\"`)."
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Reputation"
        ]
      }
    },
    "/api/v1/antispam": {
      "post": {
        "operationId": "PublicApiController_antispam",
        "summary": "Inspect a message for spam / phishing (antispam pipeline)",
        "description": "Requires an API key with the `antispam` scope.",
        "parameters": [
          {
            "name": "async",
            "required": false,
            "in": "query",
            "description": "Opt into async mode (also honours `Prefer: respond-async`). Returns 202 with a jobId to poll. NOTE: requests that include attachments are ALWAYS processed asynchronously (each attachment is deep-scanned), so they return 202 regardless of this flag.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ROUNDBOX-branded inspection verdict (synchronous mode). Act on `verdict` (clean/spam/phishing); `score` (0–100) is the confidence/severity. Cost/price is never included.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AntispamResultDto"
                }
              }
            }
          },
          "202": {
            "description": "Accepted for ASYNCHRONOUS processing — returned in async mode (`?async=true`, `Prefer: respond-async`, or `{ \"async\": true }`) and ALWAYS for any request that carries attachments. Poll `GET /api/v1/antispam/{jobId}` or await the webhook; the terminal result matches the 200 body plus a `filescan[]` entry per attachment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "jobId",
                    "filescan_job_ids"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ],
                      "example": "queued"
                    },
                    "jobId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Poll this id at GET /api/v1/antispam/{jobId}."
                    },
                    "filescan_job_ids": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "description": "One file-scan job id per attachment (empty array when the message has no attachments)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: antispam"
          },
          "429": {
            "description": "Rate limit exceeded, OR the monthly antispam quota is exhausted (tenants are notified at 80/90/100% before blocking)."
          }
        },
        "tags": [
          "Antispam"
        ]
      }
    },
    "/api/v1/antispam/{jobId}": {
      "get": {
        "operationId": "PublicApiController_antispamJob",
        "summary": "Poll an async antispam job (tenant-scoped)",
        "description": "Requires an API key with the `antispam` scope.",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "description": "The jobId returned by an async submit",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Job not found (or owned by another tenant)"
          }
        },
        "tags": [
          "Antispam"
        ]
      }
    },
    "/api/v1/files": {
      "post": {
        "operationId": "PublicFilesController_upload",
        "summary": "Upload a single file for inspection (static pipeline)",
        "description": "Requires an API key with the `filescan` scope.",
        "parameters": [
          {
            "name": "force",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rescan",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file to scan"
                  },
                  "force": {
                    "type": "boolean",
                    "description": "Bypass the Tier-0 own-TIP short-circuit and force a fresh scan"
                  },
                  "rescan": {
                    "type": "boolean",
                    "description": "Alias for force"
                  },
                  "password": {
                    "type": "string",
                    "description": "Password for an encrypted archive (sandbox-only, never persisted)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enqueue/verdict result, or a uniform { status: \"error\", error } body.",
            "content": {
              "application/json": {
                "schema": {
                  "example": {
                    "status": "queued",
                    "jobId": "9f1c…",
                    "sha256": "ab12…"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: filescan"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        },
        "tags": [
          "Malware Analysis"
        ]
      }
    },
    "/api/v1/files/dynamic": {
      "post": {
        "operationId": "PublicFilesController_dynamic",
        "summary": "Submit a file for Premium dynamic (behavioral sandbox) analysis",
        "description": "Requires an API key with the `filescan` scope.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file to detonate"
                  },
                  "password": {
                    "type": "string",
                    "description": "Password for an encrypted archive (sandbox-only, never persisted)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: filescan"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        },
        "tags": [
          "Malware Analysis"
        ]
      }
    },
    "/api/v1/files/hash": {
      "post": {
        "operationId": "PublicFilesController_hash",
        "summary": "Synchronous fused hash reputation lookup (no upload)",
        "description": "Requires an API key with the `filescan` scope.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "hash"
                ],
                "properties": {
                  "hash": {
                    "type": "string",
                    "example": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The fused, multi-source ROUNDBOX reputation report for the hash, or a uniform { status: \"error\", error } body for a malformed hash.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupReportDto"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: filescan"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Malware Analysis"
        ]
      }
    },
    "/api/v1/files/hash/lookup": {
      "post": {
        "operationId": "PublicFilesController_hashLookupSubmit",
        "summary": "Record a SHA-256 as a navigable detection; returns { sha256 }",
        "description": "Requires an API key with the `filescan` scope.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "hash"
                ],
                "properties": {
                  "hash": {
                    "type": "string",
                    "description": "64-char hex sha256 digest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "hash is not a 64-char hex sha256 digest"
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: filescan"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Malware Analysis"
        ]
      }
    },
    "/api/v1/files/{jobId}": {
      "get": {
        "operationId": "PublicFilesController_status",
        "summary": "Poll the status of an async file-scan job (tenant-scoped)",
        "description": "Requires an API key with the `filescan` scope.",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "description": "The job id returned by the upload",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current job state, phase/progress, verdict, cost, and joined file intel.",
            "content": {
              "application/json": {
                "schema": {
                  "example": {
                    "id": "9f1c…",
                    "state": "done",
                    "phase": "complete",
                    "progress": 100,
                    "verdict": {
                      "disposition": "malicious",
                      "score": 92
                    },
                    "costUsd": "0.0100",
                    "error": null,
                    "fileIntel": null
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: filescan"
          },
          "404": {
            "description": "Job not found (or owned by another tenant)"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Malware Analysis"
        ]
      }
    },
    "/api/v1/files/{jobId}/events": {
      "get": {
        "operationId": "PublicFilesController_events",
        "summary": "Server-Sent Events stream of live scan phase/progress for a job",
        "description": "Requires an API key with the `filescan` scope.",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "description": "The job id returned by the upload",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "text/event-stream of phase events"
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: filescan"
          },
          "404": {
            "description": "Job not found (or owned by another tenant)"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Malware Analysis"
        ]
      }
    },
    "/api/v1/detections": {
      "get": {
        "operationId": "PublicDetectionsController_list",
        "summary": "List this tenant’s detections (paged, filterable)",
        "description": "Requires an API key with the `detections` scope.",
        "parameters": [
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "example": 25,
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "example": 0,
              "type": "string"
            }
          },
          {
            "name": "disposition",
            "required": false,
            "in": "query",
            "description": "Filter by disposition (e.g. malicious)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "required": false,
            "in": "query",
            "description": "Filter by run kind (file-scan / dynamic-analysis / hash-lookup)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "required": false,
            "in": "query",
            "description": "ISO timestamp lower bound",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of detections joined to file intel.",
            "content": {
              "application/json": {
                "schema": {
                  "example": {
                    "total": 1,
                    "items": [
                      {
                        "sha256": "ab12…",
                        "disposition": "malicious",
                        "score": 92
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: detections"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Detections"
        ]
      }
    },
    "/api/v1/detections/{sha256}": {
      "get": {
        "operationId": "PublicDetectionsController_detail",
        "summary": "Get the detection detail record for a SHA-256",
        "description": "Requires an API key with the `detections` scope.",
        "parameters": [
          {
            "name": "sha256",
            "required": true,
            "in": "path",
            "description": "64-char hex sha256 digest",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "file_intel-shaped detail record (tenant-ownership-gated)."
          },
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: detections"
          },
          "404": {
            "description": "This tenant never scanned this hash"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Detections"
        ]
      }
    },
    "/api/v1/detections/{sha256}/runs": {
      "get": {
        "operationId": "PublicDetectionsController_runs",
        "summary": "Run timeline (last 10 completed runs) for a SHA-256",
        "description": "Requires an API key with the `detections` scope.",
        "parameters": [
          {
            "name": "sha256",
            "required": true,
            "in": "path",
            "description": "64-char hex sha256 digest",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: detections"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Detections"
        ]
      }
    },
    "/api/v1/scoring-rubric": {
      "get": {
        "operationId": "PublicDetectionsController_rubric",
        "summary": "Get the static scoring rubric (version + rules)",
        "description": "Requires an API key with the `detections` scope.",
        "parameters": [],
        "responses": {
          "401": {
            "description": "Missing or invalid API key / tenant"
          },
          "403": {
            "description": "API key is missing the required scope: detections"
          },
          "429": {
            "description": "Rate limit exceeded for this tenant"
          }
        },
        "tags": [
          "Detections"
        ]
      }
    }
  },
  "info": {
    "title": "ROUNDBOX Orbis Shield API",
    "description": "Metered Threat Intelligence Platform: reputation lookups (IP / domain / URL / dark web), malware analysis (file upload + dynamic sandbox + hash reputation), recorded detections, and message inspection (antispam / antiphishing). Every request authenticates with an API key (X-Api-Key) scoped to a tenant (X-Tenant-Id); each endpoint requires the key to carry the corresponding scope.",
    "version": "1.0.0",
    "contact": {}
  },
  "tags": [
    {
      "name": "Identity",
      "description": "Confirm an API key and view its tenant/key context (whoami)"
    },
    {
      "name": "Reputation",
      "description": "Reputation lookups for URL / IP / domain / dark-web indicators"
    },
    {
      "name": "Malware Analysis",
      "description": "File upload, dynamic sandbox, hash reputation, job status/events"
    },
    {
      "name": "Detections",
      "description": "Recorded detections, per-hash detail, run history, scoring rubric"
    },
    {
      "name": "Antispam",
      "description": "Message inspection (antispam / antiphishing)"
    }
  ],
  "servers": [
    {
      "url": "https://shield.orbis.roundbox.dev",
      "description": "Shield (dev environment)"
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key"
      },
      "TenantId": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Tenant-Id"
      }
    },
    "schemas": {
      "LookupReportDto": {
        "type": "object",
        "properties": {
          "indicator": {
            "type": "string",
            "description": "The indicator that was looked up.",
            "example": "malicious.example"
          },
          "type": {
            "type": "string",
            "description": "The indicator class that was fanned out to providers.",
            "enum": [
              "url",
              "ip",
              "domain",
              "hash"
            ],
            "example": "domain"
          },
          "verdict": {
            "type": "string",
            "description": "The fused ROUNDBOX verdict.",
            "enum": [
              "clean",
              "suspicious",
              "malicious",
              "unknown"
            ],
            "example": "malicious"
          },
          "classification": {
            "type": "string",
            "description": "Customer-facing Web/URL/Domain filter severity, derived from the verdict (clean→benign, suspicious→risky, malicious→malicious, unknown→unknown).",
            "enum": [
              "benign",
              "risky",
              "malicious",
              "unknown"
            ],
            "example": "malicious"
          },
          "classificationLabel": {
            "type": "string",
            "description": "Display label for the classification.",
            "example": "Malicious"
          },
          "nsfw": {
            "type": "boolean",
            "description": "Adult-content (NSFW) flag, shown alongside the severity classification. Derived from adult-group categories; always false for indicators with none (IPs, hashes).",
            "example": false
          },
          "score": {
            "type": "number",
            "description": "Final 0–100 risk score (higher = worse).",
            "example": 88
          },
          "rationale": {
            "type": "string",
            "description": "Henrietta (H-AI) analyst rationale — vendor-name-free.",
            "example": "ROUNDBOX Orbis Labs flags this domain as a known phishing host."
          },
          "sources": {
            "description": "ROUNDBOX Labs attribution for the contributing intelligence — underlying providers/feeds are never surfaced.",
            "example": [
              "ROUNDBOX Orbis Labs",
              "Henrietta (H-AI)"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "signals": {
            "description": "Merged, de-duped, vendor-free evidence signals.",
            "example": [
              "Listed on a phishing block-list",
              "Categorized as credential-harvesting"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "evidence": {
            "type": "object",
            "description": "Structured detail (aggregate/Henrietta scores + per-contributor summary).",
            "additionalProperties": true,
            "example": {
              "aggregateScore": 85,
              "contributingCount": 2,
              "henriettaScore": 88
            }
          }
        },
        "required": [
          "indicator",
          "type",
          "verdict",
          "classification",
          "classificationLabel",
          "nsfw",
          "score",
          "rationale",
          "sources",
          "signals",
          "evidence"
        ]
      },
      "DarkwebRecordDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Compromised email address associated with the breach record.",
            "example": "user@example.com"
          },
          "passwordMasked": {
            "type": "string",
            "description": "Password as returned by the upstream provider — always pre-masked, never unmasked or re-derived.",
            "example": "0ae6**********"
          },
          "compromiseType": {
            "type": "string",
            "description": "Type of data compromised in this record (e.g. password, hash).",
            "example": "password"
          },
          "breachDate": {
            "type": "string",
            "description": "Date the underlying breach occurred, if known.",
            "example": "2024-03-11",
            "nullable": true
          },
          "hashType": {
            "type": "string",
            "description": "Hash algorithm used for the compromised credential, if applicable.",
            "example": "bcrypt"
          }
        }
      },
      "DarkwebResultDto": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "description": "The domain this dark-web exposure check was run against.",
            "example": "acme.example"
          },
          "status": {
            "type": "string",
            "description": "The exposure verdict. 'unavailable' is distinct from 'safe' — a disabled provider or a failed/unprocessable search must never be read as 'no exposure found'.",
            "enum": [
              "safe",
              "breached",
              "unavailable"
            ],
            "example": "breached"
          },
          "breachCount": {
            "type": "number",
            "description": "Total number of compromised-credential records found for the domain.",
            "example": 2
          },
          "latestBreach": {
            "type": "string",
            "description": "Date of the most recent breach affecting this domain, if known.",
            "example": "2024-03-11",
            "nullable": true
          },
          "records": {
            "description": "Compromised-credential records for the domain. Passwords stay masked as returned by the upstream provider.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DarkwebRecordDto"
            }
          },
          "checkedAt": {
            "type": "string",
            "description": "ISO timestamp the check was performed.",
            "example": "2026-07-22T18:04:11.000Z"
          }
        },
        "required": [
          "domain",
          "status",
          "breachCount",
          "latestBreach",
          "records",
          "checkedAt"
        ]
      },
      "AttachmentDto": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string",
            "maxLength": 256,
            "example": "invoice.pdf"
          },
          "contentType": {
            "type": "string",
            "maxLength": 128,
            "example": "application/pdf"
          },
          "contentBase64": {
            "type": "string",
            "maxLength": 16777216,
            "description": "base64-encoded attachment bytes"
          }
        },
        "required": [
          "filename",
          "contentType",
          "contentBase64"
        ]
      },
      "InspectDto": {
        "type": "object",
        "properties": {
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "sms",
              "whatsapp"
            ],
            "example": "sms"
          },
          "sender": {
            "type": "string",
            "minLength": 1,
            "maxLength": 512,
            "example": "+15551234567"
          },
          "subject": {
            "type": "string",
            "maxLength": 1024,
            "example": "Your package could not be delivered"
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 50000,
            "example": "Click http://bit.ly/verify-now to reschedule your delivery."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Free-form channel metadata"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AttachmentDto"
            }
          },
          "async": {
            "type": "boolean",
            "description": "Process asynchronously: returns 202 {jobId, filescan_job_ids} to poll (equivalent to ?async=true / Prefer: respond-async). Requests WITH attachments are ALWAYS async regardless of this flag.",
            "example": false
          },
          "checkOffensiveLanguage": {
            "type": "boolean",
            "description": "Also block messages containing offensive/foul language. Adds a separate billable line ($0.001/check). Default false.",
            "example": false
          }
        },
        "required": [
          "channel",
          "sender",
          "body"
        ]
      },
      "AntispamLayerUsageDto": {
        "type": "object",
        "properties": {
          "model": {
            "type": "string",
            "description": "ROUNDBOX AI model that produced this layer (Henrietta classifier).",
            "example": "henrietta-classify"
          },
          "inputTokens": {
            "type": "number",
            "example": 165
          },
          "outputTokens": {
            "type": "number",
            "example": 59
          },
          "reasoningTokens": {
            "type": "number",
            "example": 0
          },
          "cachedInputTokens": {
            "type": "number",
            "example": 0
          }
        }
      },
      "AntispamLayerDto": {
        "type": "object",
        "properties": {
          "layer": {
            "type": "string",
            "description": "Evidence layer identifier.",
            "example": "L2"
          },
          "score": {
            "type": "number",
            "description": "This layer's risk contribution, 0–100.",
            "example": 20
          },
          "signals": {
            "description": "Human-readable evidence signals produced by this layer.",
            "example": [
              "lang:en",
              "url-shortener"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "label": {
            "type": "string",
            "description": "This layer's own classification (AI layers only).",
            "enum": [
              "clean",
              "spam",
              "phishing"
            ],
            "example": "phishing"
          },
          "rationale": {
            "type": "string",
            "description": "AI rationale for this layer (L4)."
          },
          "modelAvailable": {
            "type": "boolean",
            "description": "False when the AI model was unreachable (the check degrades to heuristics + threat-intel).",
            "example": true
          },
          "latencyMs": {
            "type": "number",
            "description": "Wall-clock latency of this layer's model/provider call, in ms.",
            "example": 1969
          },
          "usage": {
            "description": "Token usage for AI layers.",
            "allOf": [
              {
                "$ref": "#/components/schemas/AntispamLayerUsageDto"
              }
            ]
          }
        },
        "required": [
          "layer",
          "score",
          "signals"
        ]
      },
      "AntispamAttachmentDto": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string",
            "example": "invoice.pdf"
          },
          "sha256": {
            "type": "string",
            "description": "SHA-256 of the attachment.",
            "example": "e3b0c44298fc1c14…"
          },
          "disposition": {
            "type": "string",
            "description": "Hash-reputation disposition of the attachment (see note on the top-level field).",
            "enum": [
              "malicious",
              "suspicious",
              "clean",
              "unknown"
            ],
            "example": "clean"
          }
        },
        "required": [
          "filename",
          "sha256",
          "disposition"
        ]
      },
      "AntispamFilescanResultDto": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string",
            "example": "invoice.pdf"
          },
          "sha256": {
            "type": "string",
            "description": "SHA-256 of the attachment."
          },
          "jobId": {
            "type": "string",
            "description": "Child file-scan job id — poll GET /api/v1/files/:jobId."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "done",
              "failed"
            ],
            "example": "done"
          },
          "verdict": {
            "type": "object",
            "description": "Customer-safe file verdict, present when status=done (no cost fields)."
          }
        },
        "required": [
          "filename",
          "status"
        ]
      },
      "AntispamOffensiveDto": {
        "type": "object",
        "properties": {
          "severity": {
            "type": "string",
            "enum": [
              "none",
              "low",
              "medium",
              "high"
            ],
            "example": "high"
          },
          "categories": {
            "description": "Offensive-content categories found (masked evidence only).",
            "example": [
              "profanity",
              "insult"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "matchedCount": {
            "type": "number",
            "description": "How many offending items were found (count only).",
            "example": 2
          },
          "maskedRationale": {
            "type": "string",
            "description": "Masked, customer-safe explanation — never reprints the offending words.",
            "example": "Message blocked for offensive language (profanity, insult)."
          }
        },
        "required": [
          "severity",
          "categories",
          "matchedCount"
        ]
      },
      "AntispamResultDto": {
        "type": "object",
        "properties": {
          "inspectionId": {
            "type": "string",
            "description": "Unique id for this inspection — correlate it with the webhook delivery and your logs.",
            "example": "26bc4f0d-ac21-407b-bf21-bb452cd17319"
          },
          "verdict": {
            "type": "string",
            "description": "The authoritative classification to act on. It is NOT a plain score threshold, so key your logic off this field, not off `score`.",
            "enum": [
              "clean",
              "spam",
              "phishing"
            ],
            "example": "phishing"
          },
          "score": {
            "type": "number",
            "description": "Severity / confidence, 0–100 (higher = more dangerous). A strength indicator, NOT the decision: use `verdict` to act, `score` for how strong the signal is. Rough guide: <25 low, 25–59 elevated, ≥60 high.",
            "minimum": 0,
            "maximum": 100,
            "example": 46
          },
          "modelAvailable": {
            "type": "boolean",
            "description": "True when the full AI analysis was applied; false = a reduced analysis was used.",
            "example": true
          },
          "rationale": {
            "type": "string",
            "description": "Plain-language, customer-safe explanation of the verdict — safe to surface to end users.",
            "example": "Dangerous: this message uses a shortened link and pushes you to “verify” a delivery, a common scam. Do not click it; go to the delivery company’s official app instead."
          },
          "layers": {
            "description": "Per-layer evidence behind the verdict. Each entry has a layer id, a 0–100 contribution, and human-readable signal strings.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AntispamLayerDto"
            }
          },
          "urls": {
            "description": "URLs/hosts detected in the message (including any decoded from QR codes). Each is checked against ROUNDBOX threat intelligence.",
            "example": [
              "http://bit.ly/verify-now"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "lang": {
            "type": "string",
            "description": "Detected message language (ISO-639-1).",
            "example": "en"
          },
          "attachments": {
            "description": "Per-attachment reputation summary. NOTE: the antispam API inspects the message body + URLs; full malware scanning of files (including password-protected archives) is the dedicated POST /api/v1/files endpoint.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AntispamAttachmentDto"
            }
          },
          "elapsedMs": {
            "type": "number",
            "description": "Total time the whole inspection took, end to end, in milliseconds.",
            "example": 6884
          },
          "filescan": {
            "description": "Per-attachment deep file-scan results (nested file-scan jobs). Present on the async poll result/webhook; pending in the sync response.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AntispamFilescanResultDto"
            }
          },
          "filescan_job_ids": {
            "description": "Ids of the nested file-scan jobs spawned for attachments — poll each via GET /api/v1/files/:jobId.",
            "example": [
              "9f1c2e00-0000-0000-0000-000000000001"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "blocked": {
            "type": "boolean",
            "description": "True when the message was blocked by an orthogonal policy (e.g. offensive language). Key drop/allow logic off this AND `verdict`.",
            "example": true
          },
          "blockReason": {
            "type": "string",
            "description": "Why the message was blocked. Only present when `blocked` is true.",
            "enum": [
              "offensive-language"
            ],
            "example": "offensive-language"
          },
          "offensive": {
            "description": "Offensive-language check result — present only when the check ran (opt-in).",
            "allOf": [
              {
                "$ref": "#/components/schemas/AntispamOffensiveDto"
              }
            ]
          }
        },
        "required": [
          "inspectionId",
          "verdict",
          "score",
          "modelAvailable",
          "layers"
        ]
      }
    }
  },
  "security": [
    {
      "ApiKey": [],
      "TenantId": []
    }
  ]
}
