{ "openapi": "3.1.0", "info": { "title": "Scanopy API", "description": "\nSite topology discovery and visualization API.\n\n## Authentication\n\nTwo authentication methods are supported:\n\n| Method | Header | Use Case |\n|--------|--------|----------|\n| User API key | `Authorization: Bearer scp_u_...` | Programmatic access, integrations |\n| Session cookie | `Cookie: session_id=...` | Web UI (via `/api/auth/login`) |\n\nUser API keys require your organization to have API access enabled. Create keys at **Platform > API Keys**.\n\n## Rate Limiting\n\nLimit: 300 requests/minute\n\nBurst: 150\n\nResponse headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`\n\nWhen rate limited, you'll receive HTTP `429 Too Many Requests` with a `Retry-After` header.\n\n## Pagination\n\nList endpoints support pagination via query parameters:\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `limit` | integer | 50 | Maximum results to return (1-1000). Use 0 for no limit. |\n| `offset` | integer | 0 | Number of results to skip |\n\nExample: `GET /api/v1/hosts?limit=10&offset=20`\n\n## Response Format\n\nAll responses use a standard envelope:\n\n```json\n{\n \"success\": true,\n \"data\": { ... },\n \"meta\": {\n \"api_version\": 1,\n \"server_version\": \"0.19.0\"\n }\n}\n```\n\n**Paginated list responses** include pagination metadata:\n\n```json\n{\n \"success\": true,\n \"data\": [ ... ],\n \"meta\": {\n \"api_version\": 1,\n \"server_version\": \"0.19.0\",\n \"pagination\": {\n \"total_count\": 142,\n \"limit\": 50,\n \"offset\": 0,\n \"has_more\": true\n }\n }\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `total_count` | Total items matching your query (ignoring pagination) |\n| `limit` | Applied limit (your request or default) |\n| `offset` | Applied offset |\n| `has_more` | `true` if more results exist beyond this page |\n\n**Error responses** include an `error` field instead of `data`:\n\n```json\n{\n \"success\": false,\n \"error\": \"Resource not found\",\n \"meta\": { ... }\n}\n```\n\n**Common status codes:** `400` validation error, `401` unauthorized, `403` forbidden, `404` not found, `409` conflict, `429` rate limited.\n\n## Versioning\n\nEndpoints are prefixed with `/api/v1/`. The API version is an integer (`api_version: 1`) returned in every response, versioned independently from the application. Check `GET /api/version` for current versions.\n\n**While Scanopy is pre-v1.0 (current: 0.19.0)**, the API should be considered unstable. Breaking changes may be introduced in any release without incrementing the API version. We recommend pinning to a specific Scanopy release if you depend on API stability, and reviewing the [changelog](/changelog) before upgrading. After Scanopy reaches v1.0, breaking API changes will only occur with an API version increment.\n\n## Multi-Tenancy\n\nResources are scoped to your **organization** and **site(s)**:\n\n- You can only access entities within your organization\n- Site-level entities (hosts, services, etc.) are filtered to sites you have access to\n- Use `?site_id=` to filter list endpoints to a specific site\n- API keys can be scoped to a subset of your accessible sites\n", "license": { "name": "Dual (AGPL3.0, Commercial License Available)" }, "version": "1" }, "servers": [ { "url": "https://app.scanopy.net", "description": "Scanopy Cloud" }, { "url": "{scheme}://{host}", "description": "Self-hosted server", "variables": { "host": { "default": "scanopy.example.com", "description": "Host and optional port of your Scanopy server" }, "scheme": { "default": "https", "enum": [ "https", "http" ] } } } ], "paths": { "/api/auth/accept-terms": { "post": { "tags": [ "auth", "internal" ], "operationId": "accept_terms", "responses": { "200": { "description": "Terms accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/check-email": { "post": { "tags": [ "auth", "internal" ], "operationId": "check_email", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckEmailRequest" } } }, "required": true }, "responses": { "200": { "description": "Whether the address is available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_CheckEmailResponse" } } } }, "403": { "description": "Password login is disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/forgot-password": { "post": { "tags": [ "auth", "internal" ], "operationId": "forgot_password", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ForgotPasswordRequest" } } }, "required": true }, "responses": { "200": { "description": "Password reset email sent", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } } } }, "/api/auth/login": { "post": { "tags": [ "auth", "internal" ], "operationId": "login", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginRequest" } } }, "required": true }, "responses": { "200": { "description": "Login successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "401": { "description": "Invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Login forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/logout": { "post": { "tags": [ "auth", "internal" ], "operationId": "logout", "responses": { "200": { "description": "Logout successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } } } }, "/api/auth/me": { "post": { "tags": [ "auth", "internal" ], "operationId": "get_current_user", "responses": { "200": { "description": "Current user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/oidc/{slug}/unlink": { "post": { "tags": [ "auth", "internal" ], "operationId": "unlink_oidc_account", "parameters": [ { "name": "slug", "in": "path", "description": "OIDC provider slug", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "OIDC account unlinked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Blocked in demo mode", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Provider not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/onboarding-state": { "get": { "tags": [ "auth", "internal" ], "summary": "Get current onboarding state from session", "operationId": "onboarding_state", "responses": { "200": { "description": "Onboarding state", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_OnboardingStateResponse" } } } } } } }, "/api/auth/onboarding-step": { "post": { "tags": [ "auth", "internal" ], "summary": "Store onboarding step in session", "operationId": "onboarding_step", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OnboardingStepRequest" } } }, "required": true }, "responses": { "200": { "description": "Step saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } } } }, "/api/auth/register": { "post": { "tags": [ "auth", "internal" ], "operationId": "register", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RegisterRequest" } } }, "required": true }, "responses": { "200": { "description": "User registered successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Registration disabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Email already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/request-email-change": { "post": { "tags": [ "auth", "internal" ], "operationId": "request_email_change", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RequestEmailChangeRequest" } } }, "required": true }, "responses": { "200": { "description": "Verification email sent to new address", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/resend-verification": { "post": { "tags": [ "auth", "internal" ], "operationId": "resend_verification", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResendVerificationRequest" } } }, "required": true }, "responses": { "200": { "description": "Accepted. A link is sent only when the address has an unverified account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Invalid request, or a link was sent to this address less than a minute ago", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/reset-password": { "post": { "tags": [ "auth", "internal" ], "operationId": "reset_password", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResetPasswordRequest" } } }, "required": true }, "responses": { "200": { "description": "Password reset successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "400": { "description": "Invalid or expired token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/setup": { "post": { "tags": [ "auth", "internal" ], "summary": "Store pre-registration setup data (org name, first site) in session", "operationId": "setup", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetupRequest" } } }, "required": true }, "responses": { "200": { "description": "Setup data stored", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_SetupResponse" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/update": { "post": { "tags": [ "auth", "internal" ], "operationId": "update_password_auth", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdatePasswordRequest" } } }, "required": true }, "responses": { "200": { "description": "Password updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Blocked in demo mode", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/auth/verify-email": { "post": { "tags": [ "auth", "internal" ], "operationId": "verify_email", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VerifyEmailRequest" } } }, "required": true }, "responses": { "200": { "description": "Email verified successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "400": { "description": "Invalid or expired token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/billing/cancel": { "post": { "tags": [ "billing", "internal" ], "summary": "Cancel subscription", "description": "In-app cancel modal endpoint. Sets Stripe `cancel_at` to the current\nperiod end (via Stripe's `MaxPeriodEnd` sentinel), stashes the canonical\nScanopy reason in subscription metadata, returns the period end so the\nmodal can render the retention disclosure.", "operationId": "cancel_subscription", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelSubscriptionRequest" } } }, "required": true }, "responses": { "200": { "description": "Cancellation initiated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_CancelSubscriptionResponse" } } } }, "400": { "description": "No active subscription or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/cancel/apply-discount": { "post": { "tags": [ "billing", "internal" ], "summary": "Apply the discount save offer", "description": "Applies the configured Stripe coupon to the subscription. Returns 400\nwhen `STRIPE_SAVE_OFFER_COUPON_ID` is unset.", "operationId": "apply_discount_save_offer", "responses": { "200": { "description": "Discount applied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "Discount not configured or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/change-plan": { "post": { "tags": [ "billing", "internal" ], "summary": "Change billing plan", "description": "Upgrades or downgrades the organization's billing plan.", "operationId": "change_plan", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChangePlanRequest" } } }, "required": true }, "responses": { "200": { "description": "Plan change initiated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "Invalid plan or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/change-plan/preview": { "get": { "tags": [ "billing", "internal" ], "summary": "Preview plan change (shows overage counts)", "operationId": "preview_plan_change", "parameters": [ { "name": "plan", "in": "query", "description": "Target plan (JSON)", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Plan change preview", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_ChangePlanPreview" } } } }, "400": { "description": "Billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/checkout": { "post": { "tags": [ "billing", "internal" ], "summary": "Create a checkout session", "operationId": "create_checkout_session", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCheckoutRequest" } } }, "required": true }, "responses": { "200": { "description": "Checkout session URL", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "Invalid plan or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/end-trial": { "post": { "tags": [ "billing", "internal" ], "summary": "End the trial now and charge the card on file", "description": "Offered to customers who want an air-gapped license key, which needs a paid\nsubscription because it validates offline and cannot be revoked.", "operationId": "end_trial", "responses": { "200": { "description": "Trial ended and subscription charged", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "Not trialing, no payment method, or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/extend-trial": { "post": { "tags": [ "billing", "internal" ], "summary": "Self-serve trial extend (+7 days, once per org lifetime)", "operationId": "extend_trial", "responses": { "200": { "description": "Trial extended", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "Ineligible or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/finalize-payment-method": { "post": { "tags": [ "billing", "internal" ], "summary": "Finalize a client-confirmed SetupIntent (set the card as default)", "operationId": "finalize_payment_method", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FinalizePaymentMethodRequest" } } }, "required": true }, "responses": { "200": { "description": "Payment method finalized", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Billing not enabled or SetupIntent invalid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/invoice-billing": { "get": { "tags": [ "billing", "internal" ], "summary": "Invoice billing status", "description": "Whether the organization pays by invoice, the PO number on its invoices,\nthe unpaid invoice, and any open quote.", "operationId": "get_invoice_billing_status", "responses": { "200": { "description": "Invoice billing status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_InvoiceBillingStatus" } } } }, "400": { "description": "Billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "billing", "internal" ], "summary": "Pay by invoice", "description": "Records the billing entity on the organization's billing account, then\neither issues the first invoice now or opens a quote for the buyer's\nprocurement to raise a purchase order against. Self-hosted plans only.", "operationId": "set_up_invoice_billing", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvoiceBillingRequest" } } }, "required": true }, "responses": { "200": { "description": "Invoice sent or quote opened", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "Not a self-hosted plan, invalid details, or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/pause": { "post": { "tags": [ "billing", "internal" ], "summary": "Pause subscription billing", "description": "Pauses billing for a 30/60/90 day window. Eligibility: rolling 6-month\ncooldown anchored on the org's `last_paused_at`.", "operationId": "pause_subscription", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PauseSubscriptionRequest" } } }, "required": true }, "responses": { "200": { "description": "Subscription paused", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "Ineligible or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/payment-method-setup-intent": { "post": { "tags": [ "billing", "internal" ], "summary": "Create a SetupIntent for in-app card collection (Stripe Payment Element)", "operationId": "create_payment_method_setup_intent", "responses": { "200": { "description": "SetupIntent client secret", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_SetupIntentResponse" } } } }, "400": { "description": "Billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/plans": { "get": { "tags": [ "billing", "internal" ], "summary": "Get available billing plans", "operationId": "get_billing_plans", "responses": { "200": { "description": "List of available billing plans", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_BillingPlan" } } } }, "400": { "description": "Billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/po-number": { "put": { "tags": [ "billing", "internal" ], "summary": "Update the PO number", "description": "Replaces the purchase order number printed on this organization's future\ninvoices, for a renewal raised against a new PO.", "operationId": "update_po_number", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdatePoNumberRequest" } } }, "required": true }, "responses": { "200": { "description": "PO number updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Not a self-hosted plan or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/portal": { "post": { "tags": [ "billing", "internal" ], "summary": "Create a billing portal session", "operationId": "create_portal_session", "requestBody": { "content": { "text/plain": { "schema": { "type": "string" } } }, "required": true }, "responses": { "200": { "description": "Portal session URL", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "Billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/quote": { "delete": { "tags": [ "billing", "internal" ], "summary": "Cancel the open quote", "operationId": "cancel_quote", "responses": { "200": { "description": "Quote cancelled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "No open quote or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/quote/accept": { "post": { "tags": [ "billing", "internal" ], "summary": "Accept the open quote", "description": "Stripe creates the invoiced subscription from the quote and sends the\nfirst invoice, carrying whatever purchase order number the billing account\nholds (set through `PUT /po-number`).", "operationId": "accept_quote", "responses": { "200": { "description": "Quote accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "No open quote or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/quote/pdf": { "get": { "tags": [ "billing", "internal" ], "summary": "Download the open quote as a PDF", "operationId": "download_quote_pdf", "responses": { "200": { "description": "Quote PDF", "content": { "application/pdf": { "schema": { "type": "array", "items": { "type": "integer", "format": "int32", "minimum": 0 } } } } }, "400": { "description": "No open quote or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/reactivate": { "post": { "tags": [ "billing", "internal" ], "summary": "Reactivate a subscription pending cancellation", "description": "Clears Stripe's scheduled-cancellation state (`cancel_at` → None).\nAvailable while `plan_status === 'pending_cancellation'`.", "operationId": "reactivate_subscription", "responses": { "200": { "description": "Subscription reactivated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "No pending cancellation or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/resume": { "post": { "tags": [ "billing", "internal" ], "summary": "Resume a paused subscription", "description": "Clears Stripe pause collection and re-activates billing. Available while\n`plan_status === 'paused'`. The prorated pause credit is posted to the\ncustomer's Stripe balance asynchronously by the webhook arm that fires\nfor the `pause_collection` clear — the endpoint just returns success.", "operationId": "resume_subscription", "responses": { "200": { "description": "Subscription resumed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "400": { "description": "No paused subscription or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/save-offer-coupon": { "get": { "tags": [ "billing", "internal" ], "summary": "Read live terms for the configured save-offer coupon", "description": "Returns the coupon's `percent_off` and `duration_in_months` so the\ncancel modal's Discount panel can render the offer dynamically. The\npayload is `null` when `STRIPE_SAVE_OFFER_COUPON_ID` is unset — the\nmodal hides the panel in that case.", "operationId": "get_save_offer_coupon", "responses": { "200": { "description": "Save-offer coupon terms, or null when not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Option_SaveOfferCoupon" } } } }, "400": { "description": "Billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/billing/webhooks": { "post": { "tags": [ "billing", "internal" ], "summary": "Handle Stripe webhook", "description": "Internal endpoint for Stripe webhook callbacks.", "operationId": "handle_webhook", "responses": { "200": { "description": "Webhook processed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Invalid signature or billing not enabled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/config": { "get": { "tags": [ "config", "internal" ], "summary": "Get public server configuration", "description": "Returns public configuration settings like OIDC providers, billing status, etc.", "operationId": "get_public_config", "responses": { "200": { "description": "Public server configuration", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_PublicConfigResponse" } } } } } } }, "/api/daemons/register": { "post": { "tags": [ "Daemons", "internal" ], "summary": "Register a new Daemon", "description": "Internal endpoint for daemon self-registration. Creates a host entry\nand sets up default discovery jobs for the daemon.", "operationId": "register_daemon", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DaemonRegistrationRequest" } } }, "required": true }, "responses": { "200": { "description": "Daemon registered successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DaemonRegistrationResponse" } } } }, "403": { "description": "Daemon registration disabled in demo mode", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "daemon_api_key": [] } ] } }, "/api/daemons/{id}/heartbeat": { "post": { "tags": [ "Daemons", "internal", "deprecated" ], "summary": "Receive daemon heartbeat (DEPRECATED - for backwards compatibility with pre-v0.14.0 daemons)", "description": "Internal endpoint for legacy daemons to send periodic heartbeats.\nNew daemons (v0.14.0+) use the /request-work endpoint which includes heartbeat functionality.\nThis endpoint is kept for backwards compatibility and will be removed in a future version.", "operationId": "receive_heartbeat", "parameters": [ { "name": "id", "in": "path", "description": "Daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DaemonHeartbeatPayload" } } }, "required": true }, "responses": { "200": { "description": "Heartbeat received", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "daemon_api_key": [] } ] } }, "/api/daemons/{id}/request-work": { "post": { "tags": [ "Daemons", "internal" ], "summary": "Request work from server", "description": "Internal endpoint for daemons to poll for pending discovery sessions.\nAlso updates heartbeat and returns any pending cancellation requests.\nReturns tuple of (next_session, should_cancel).", "operationId": "receive_work_request", "parameters": [ { "name": "id", "in": "path", "description": "Daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DaemonStatus" } } }, "required": true }, "responses": { "200": { "description": "Work request processed - returns (Option, bool)" }, "404": { "description": "Daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "daemon_api_key": [] } ] } }, "/api/daemons/{id}/startup": { "post": { "tags": [ "Daemons", "internal" ], "summary": "Daemon startup handshake", "description": "Internal endpoint for daemons to report their version on startup.\nUpdates the daemon's version and last_seen timestamp, returns server capabilities.", "operationId": "daemon_startup", "parameters": [ { "name": "id", "in": "path", "description": "Daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DaemonStartupRequest" } } }, "required": true }, "responses": { "200": { "description": "Startup acknowledged", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_ServerCapabilities" } } } }, "404": { "description": "Daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "daemon_api_key": [] } ] } }, "/api/daemons/{id}/update-capabilities": { "post": { "tags": [ "Daemons", "internal" ], "summary": "Update Daemon capabilities", "description": "Legacy internal endpoint for pre-0.15 daemons to report their interfaced\nsubnets as bare ids. Modern daemons report them via the status heartbeat's\n`interfaced_subnets` channel; this remains functional so older daemons in a\nrolling deploy keep reporting (and don't 404).", "operationId": "update_capabilities", "parameters": [ { "name": "id", "in": "path", "description": "Daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LegacyCapabilities" } } }, "required": true }, "responses": { "200": { "description": "Capabilities updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "daemon_api_key": [] } ] } }, "/api/email/feedback/sns": { "post": { "tags": [ "internal" ], "summary": "Receive SES feedback from Amazon SNS", "description": "The HTTPS subscription endpoint for the SNS topic named by\n`SCANOPY_EMAIL_FEEDBACK_SNS_TOPIC_ARN`. Confirms the subscription, and\napplies SES bounce and complaint notifications. Answers 404 when no topic\nis configured.", "operationId": "receive_sns_feedback", "requestBody": { "description": "SNS message JSON", "content": { "text/plain": { "schema": { "type": "string" } } }, "required": true }, "responses": { "200": { "description": "Message processed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Not an authentic message for the configured topic", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "SES feedback is not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/email/unsubscribe/{token}/{category}": { "get": { "tags": [ "internal" ], "summary": "Open an unsubscribe link in a browser", "description": "A mail client that does not support one-click opens the `List-Unsubscribe`\nURL with a GET. That changes nothing: it redirects to Settings → Email,\nwhere the user chooses what to turn off.", "operationId": "open_unsubscribe_link", "parameters": [ { "name": "token", "in": "path", "description": "The recipient's unsubscribe token", "required": true, "schema": { "type": "string" } }, { "name": "category", "in": "path", "description": "Pausable email category, e.g. discovery_digest", "required": true, "schema": { "type": "string" } } ], "responses": { "303": { "description": "Redirect to the email preferences page" } } }, "post": { "tags": [ "internal" ], "summary": "One-click unsubscribe", "description": "Turns off one pausable email category for the user the token belongs to.\nMail clients POST here from the `List-Unsubscribe` header. Answers 200\nwhether or not the token matched, so the endpoint does not reveal which\ntokens exist. The request body is ignored.", "operationId": "unsubscribe", "parameters": [ { "name": "token", "in": "path", "description": "The recipient's unsubscribe token", "required": true, "schema": { "type": "string" } }, { "name": "category", "in": "path", "description": "Pausable email category, e.g. discovery_digest", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Request accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Unknown email category", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/github-stars": { "get": { "tags": [ "github", "internal" ], "summary": "Get GitHub star count", "description": "Returns the current star count for the Scanopy GitHub repository.\nDeprecated: nothing in the UI calls it. Both `GithubStars` mounts are\ncommented out, and the route stays so they can come back.", "operationId": "get_stars", "responses": { "200": { "description": "GitHub star count", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_u32" } } } } }, "deprecated": true } }, "/api/install/windows-msi": { "get": { "tags": [ "Daemons" ], "summary": "Download the Windows daemon MSI", "description": "Proxies the Windows daemon MSI from GitHub so the browser can save it under a caller-chosen filename. Cached for an hour to avoid refetching from GitHub on every request.", "operationId": "get_windows_msi", "responses": { "200": { "description": "Windows daemon MSI installer", "content": { "application/octet-stream": { "schema": { "type": "array", "items": { "type": "integer", "format": "int32", "minimum": 0 } } } } }, "502": { "description": "Failed to fetch the MSI from GitHub", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/v1/auth/daemon": { "get": { "tags": [ "Daemon API Keys" ], "summary": "List all Daemon API Keys", "operationId": "list_daemon_api_keys", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "unbound", "in": "query", "description": "`true` returns only keys no daemon is bound to, the ones managed from the API keys page;\n`false` only keys bound to a daemon. Omit for both.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "is_enabled", "in": "query", "description": "Only enabled keys (`true`), disabled ones (`false`), or both (repeat it). Sent empty,\nmatches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns keys that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the key must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the key's name.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of Daemon API Keys", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/DaemonApiKey" }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Daemon API Keys" ], "summary": "Create Daemon API Key", "operationId": "create_daemon_api_key", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DaemonApiKey" } } }, "required": true }, "responses": { "200": { "description": "Daemon API key created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DaemonApiKeyResponse" } } } }, "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Insufficient permissions (member+ required)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/auth/daemon/bulk-delete": { "post": { "tags": [ "Daemon API Keys" ], "summary": "Bulk delete daemon_api_keys", "description": "Returns 409 Conflict if any key is currently assigned to a daemon.", "operationId": "bulk_delete_daemon_api_keys", "requestBody": { "description": "Array of Daemon API Key IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "daemon_api_keys deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } }, "409": { "description": "One or more API keys are in use by daemons", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/auth/daemon/export/csv": { "get": { "tags": [ "Daemon API Keys" ], "summary": "Export Daemon API Keys to CSV", "description": "Export all Daemon API Keys matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_daemon_api_keys_csv", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "unbound", "in": "query", "description": "`true` returns only keys no daemon is bound to, the ones managed from the API keys page;\n`false` only keys bound to a daemon. Omit for both.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "is_enabled", "in": "query", "description": "Only enabled keys (`true`), disabled ones (`false`), or both (repeat it). Sent empty,\nmatches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns keys that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the key must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the key's name.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing Daemon API Keys", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/auth/daemon/field-values/{field}": { "get": { "tags": [ "Daemon API Keys" ], "summary": "List the values of a Daemon API Key field", "description": "Every distinct value of one Daemon API Key field across the Daemon API Keys the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_daemon_api_key_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/DaemonApiKeyOrderField" } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "unbound", "in": "query", "description": "`true` returns only keys no daemon is bound to, the ones managed from the API keys page;\n`false` only keys bound to a daemon. Omit for both.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "is_enabled", "in": "query", "description": "Only enabled keys (`true`), disabled ones (`false`), or both (repeat it). Sent empty,\nmatches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns keys that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the key must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the key's name.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Distinct values and their Daemon API Key counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/auth/daemon/{id}": { "get": { "tags": [ "Daemon API Keys" ], "summary": "Get Daemon API Key by ID", "operationId": "get_daemon_api_key_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Daemon API Key ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Daemon API Key found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DaemonApiKey" } } } }, "404": { "description": "Daemon API Key not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Daemon API Keys" ], "summary": "Update a Daemon API Key", "operationId": "update_daemon_api_key", "parameters": [ { "name": "id", "in": "path", "description": "Daemon API key ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DaemonApiKey" } } }, "required": true }, "responses": { "200": { "description": "Daemon API key updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DaemonApiKey" } } } }, "404": { "description": "Daemon API key not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Daemon API Keys" ], "summary": "Delete daemon_api_key", "description": "Returns 409 Conflict if the key is currently assigned to a daemon.", "operationId": "delete_daemon_api_key", "parameters": [ { "name": "id", "in": "path", "description": "daemon_api_key ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "daemon_api_key deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "daemon_api_key not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "API key is in use by a daemon", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/auth/daemon/{id}/rotate": { "post": { "tags": [ "Daemon API Keys" ], "summary": "Rotate a Daemon API Key", "operationId": "rotate_key_handler", "parameters": [ { "name": "id", "in": "path", "description": "Daemon API key ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Daemon API key rotated, returns new key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "404": { "description": "Daemon API key not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/auth/keys": { "get": { "tags": [ "User API Keys" ], "summary": "Get all user API keys for the current user", "operationId": "get_all_user_api_keys", "parameters": [ { "name": "permissions", "in": "query", "description": "Only keys limited to one of these roles. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/UserOrgPermissions" } } }, { "name": "site_ids", "in": "query", "description": "Only keys with access to one of these sites. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "is_enabled", "in": "query", "description": "Only enabled keys (`true`), disabled ones (`false`), or both (repeat it). Sent empty, matches\nnone.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns keys that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the key must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the key's name.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/UserApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/UserApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of user API keys", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_UserApiKey" } } } }, "401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] }, "post": { "tags": [ "User API Keys" ], "summary": "Create a new user API key", "operationId": "create_user_api_key", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserApiKey" } } }, "required": true }, "responses": { "200": { "description": "API key created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_UserApiKeyResponse" } } } }, "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Invalid permissions or site access", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/auth/keys/bulk-delete": { "post": { "tags": [ "User API Keys" ], "summary": "Bulk delete user API keys", "operationId": "bulk_delete_user_api_keys", "requestBody": { "description": "Array of User API Key IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "API keys deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/auth/keys/export/csv": { "get": { "tags": [ "User API Keys" ], "summary": "Export User API Keys to CSV", "description": "Export all User API Keys matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_user_api_keys_csv", "parameters": [ { "name": "permissions", "in": "query", "description": "Only keys limited to one of these roles. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/UserOrgPermissions" } } }, { "name": "site_ids", "in": "query", "description": "Only keys with access to one of these sites. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "is_enabled", "in": "query", "description": "Only enabled keys (`true`), disabled ones (`false`), or both (repeat it). Sent empty, matches\nnone.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns keys that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the key must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the key's name.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/UserApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/UserApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing User API Keys", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/auth/keys/field-values/{field}": { "get": { "tags": [ "User API Keys" ], "summary": "List the values of a User API Key field", "description": "Every distinct value of one User API Key field across the User API Keys the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_user_api_key_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/UserApiKeyOrderField" } }, { "name": "permissions", "in": "query", "description": "Only keys limited to one of these roles. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/UserOrgPermissions" } } }, { "name": "site_ids", "in": "query", "description": "Only keys with access to one of these sites. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "is_enabled", "in": "query", "description": "Only enabled keys (`true`), disabled ones (`false`), or both (repeat it). Sent empty, matches\nnone.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns keys that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the key must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the key's name.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/UserApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/UserApiKeyOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Distinct values and their User API Key counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/auth/keys/{id}": { "get": { "tags": [ "User API Keys" ], "summary": "Get a user API key by ID", "operationId": "get_user_api_key_by_id", "parameters": [ { "name": "id", "in": "path", "description": "API key ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "API key found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_UserApiKey" } } } }, "404": { "description": "API key not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] }, "put": { "tags": [ "User API Keys" ], "summary": "Update a user API key", "operationId": "update_user_api_key", "parameters": [ { "name": "id", "in": "path", "description": "API key ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserApiKey" } } }, "required": true }, "responses": { "200": { "description": "API key updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_UserApiKey" } } } }, "403": { "description": "Not authorized to update this key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "API key not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] }, "delete": { "tags": [ "User API Keys" ], "summary": "Delete a user API key", "operationId": "delete_user_api_key", "parameters": [ { "name": "id", "in": "path", "description": "API key ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "API key deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "API key not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/auth/keys/{id}/rotate": { "post": { "tags": [ "User API Keys" ], "summary": "Rotate a user API key", "operationId": "rotate_user_api_key", "parameters": [ { "name": "id", "in": "path", "description": "API key ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "API key rotated, returns new key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_String" } } } }, "403": { "description": "Not authorized to rotate this key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "API key not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/bindings": { "get": { "tags": [ "Bindings" ], "summary": "List all Bindings", "operationId": "list_bindings", "parameters": [ { "name": "service_id", "in": "query", "description": "Filter by service ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "port_id", "in": "query", "description": "Filter by port ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ip_address_id", "in": "query", "description": "Filter by interface ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "List of Bindings", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Binding" }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Bindings" ], "summary": "Create a new Binding", "description": "Creates a binding that associates a service with a port or interface.\n\n### Binding Types\n\n- **Interface binding**: Service is present at an interface (IP address) without a specific port.\n Used for non-port-bound services like gateways.\n- **Port binding (specific ip_address)**: Service listens on a specific port on a specific interface.\n- **Port binding (all ip_addresses)**: Service listens on a specific port on all ip_addresses\n (`ip_address_id: null`).\n\n### Validation and Deduplication Rules\n\n- **Conflict detection**: Interface bindings conflict with port bindings on the same interface.\n A port binding on all ip_addresses conflicts with any interface binding for the same service.\n- **All-interfaces precedence**: When creating a port binding with `ip_address_id: null`,\n any existing specific-interface bindings for the same port are automatically removed,\n as they are superseded by the all-interfaces binding.", "operationId": "create_binding", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Binding" } } }, "required": true }, "responses": { "200": { "description": "Binding created (superseded bindings may be removed)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Binding" } } } }, "400": { "description": "Referenced port or ip_address does not exist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Conflict with existing binding type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/bindings/bulk-delete": { "post": { "tags": [ "Bindings" ], "summary": "Bulk delete Bindings", "operationId": "bulk_delete_bindings", "requestBody": { "description": "Array of Binding IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Bindings deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/bindings/export/csv": { "get": { "tags": [ "Bindings" ], "summary": "Export Bindings to CSV", "description": "Export all Bindings matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_bindings_csv", "parameters": [ { "name": "service_id", "in": "query", "description": "Filter by service ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "port_id", "in": "query", "description": "Filter by port ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ip_address_id", "in": "query", "description": "Filter by interface ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "CSV file containing Bindings", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/bindings/{id}": { "get": { "tags": [ "Bindings" ], "summary": "Get Binding by ID", "operationId": "get_binding_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Binding ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Binding found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Binding" } } } }, "404": { "description": "Binding not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Bindings" ], "summary": "Update a Binding", "description": "Updates an existing binding. The same conflict detection rules from binding creation apply.\n\n## Validation Rules\n\n- **Conflict detection**: The updated binding must not conflict with other bindings on the\n same service. Interface bindings conflict with port bindings on the same interface.", "operationId": "update_binding", "parameters": [ { "name": "id", "in": "path", "description": "Binding ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Binding" } } }, "required": true }, "responses": { "200": { "description": "Binding updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Binding" } } } }, "400": { "description": "Referenced port or ip_address does not exist", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Conflict with existing binding type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Bindings" ], "summary": "Delete Binding", "operationId": "delete_binding", "parameters": [ { "name": "id", "in": "path", "description": "Binding ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Binding deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Binding not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/credentials": { "get": { "tags": [ "Credentials" ], "summary": "List all Credentials", "description": "Returns all credentials in the authenticated user's organization.\nOptionally filter by type (e.g. `?type=SnmpV2c`).", "operationId": "get_all_credentials", "parameters": [ { "name": "type", "in": "query", "description": "Filter by credential type (e.g. `SnmpV2c`, `DockerProxy`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/CredentialTypeDiscriminants" } } }, { "name": "integrations", "in": "query", "description": "Only credentials of one of these integrations. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/CredentialIntegration" } } }, { "name": "beta", "in": "query", "description": "Only credentials whose type is in beta (`true`), stable (`false`), or both (repeat it).\nSent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "unofficial_api", "in": "query", "description": "Only credentials whose type reads an unpublished API (`true`), a published one (`false`),\nor both (repeat it). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "targets", "in": "query", "description": "Only credentials whose type can be applied to one of these targets. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/Target" } } }, { "name": "daemon_os", "in": "query", "description": "Only file and socket credentials for one of these daemon OS families. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/OsFamily" } } }, { "name": "site_ids", "in": "query", "description": "Only credentials assigned to one of these sites. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns credentials that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the credential must hold one tag from each of. Each entry is a comma-separated\nlist of tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and description.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/CredentialOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/CredentialOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Credential" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Credentials" ], "summary": "Create a new Credential", "description": "Creates a credential scoped to your organization.", "operationId": "create_credential", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Credential" } } }, "required": true }, "responses": { "200": { "description": "Credential created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Credential" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/credentials/bulk": { "post": { "tags": [ "Credentials" ], "summary": "Bulk create Credentials", "description": "Creates multiple credentials in one request. Validation is atomic — if any\ncredential has an invalid type, none are created. Individual creates are\nsequential, so a mid-batch DB error leaves earlier credentials committed.", "operationId": "bulk_create_credentials", "requestBody": { "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Credential" } } } }, "required": true }, "responses": { "200": { "description": "Credentials created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_Credential" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/credentials/bulk-delete": { "post": { "tags": [ "Credentials" ], "summary": "Bulk delete Credentials", "operationId": "bulk_delete_credentials", "requestBody": { "description": "Array of Credential IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Credentials deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/credentials/export/csv": { "get": { "tags": [ "Credentials" ], "summary": "Export Credentials to CSV", "description": "Export all Credentials matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_credentials_csv", "parameters": [ { "name": "type", "in": "query", "description": "Filter by credential type (e.g. `SnmpV2c`, `DockerProxy`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/CredentialTypeDiscriminants" } } }, { "name": "integrations", "in": "query", "description": "Only credentials of one of these integrations. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/CredentialIntegration" } } }, { "name": "beta", "in": "query", "description": "Only credentials whose type is in beta (`true`), stable (`false`), or both (repeat it).\nSent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "unofficial_api", "in": "query", "description": "Only credentials whose type reads an unpublished API (`true`), a published one (`false`),\nor both (repeat it). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "targets", "in": "query", "description": "Only credentials whose type can be applied to one of these targets. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/Target" } } }, { "name": "daemon_os", "in": "query", "description": "Only file and socket credentials for one of these daemon OS families. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/OsFamily" } } }, { "name": "site_ids", "in": "query", "description": "Only credentials assigned to one of these sites. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns credentials that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the credential must hold one tag from each of. Each entry is a comma-separated\nlist of tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and description.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/CredentialOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/CredentialOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing Credentials", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/credentials/{id}": { "get": { "tags": [ "Credentials" ], "summary": "Get a Credential by ID", "operationId": "get_by_id_credential", "parameters": [ { "name": "id", "in": "path", "description": "Credential ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Credential found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Credential" } } } }, "404": { "description": "Credential not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Credentials" ], "summary": "Update Credential", "operationId": "update_credential", "parameters": [ { "name": "id", "in": "path", "description": "Credential ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Credential" } } }, "required": true }, "responses": { "200": { "description": "Credential updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Credential" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Credential not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Credentials" ], "summary": "Delete Credential", "operationId": "delete_credential", "parameters": [ { "name": "id", "in": "path", "description": "Credential ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Credential deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Credential not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons": { "get": { "tags": [ "Daemons" ], "summary": "Get all daemons", "description": "Returns all daemons accessible to the user.\nSupports pagination via `limit` and `offset` query parameters,\nand ordering via `group_by`, `order_by`, and `order_direction`.", "operationId": "get_daemons", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "statuses", "in": "query", "description": "Only daemons in one of these statuses. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonHealth" } } }, { "name": "os", "in": "query", "description": "Only daemons installed on one of these operating systems. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonOs" } } }, { "name": "modes", "in": "query", "description": "Only daemons that connect one of these ways. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonMode" } } }, { "name": "maintainer_ids", "in": "query", "description": "Only daemons maintained by one of these users. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "versions", "in": "query", "description": "Only daemons running one of these versions. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "interfaced_subnet_ids", "in": "query", "description": "Only daemons with an interface on one of these subnets. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns daemons that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the daemon must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and url.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of daemons", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_DaemonResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/bulk-delete": { "post": { "tags": [ "Daemons" ], "summary": "Bulk delete daemons", "operationId": "bulk_delete_daemons", "requestBody": { "description": "Array of Daemon IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "daemons deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } }, "409": { "description": "daemon has active sessions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/bulk-retry-connection": { "post": { "tags": [ "Daemons" ], "summary": "Retry connection to unreachable daemons", "description": "Resets the is_unreachable flag on each daemon, as `POST /{id}/retry-connection` does for one.\nThe poller contacts each again on its next cycle; one that is still down is marked\nunreachable again, and its unreachable notification is sent again. The request is refused,\nand nothing changes, if any daemon isn't a ServerPoll daemon.", "operationId": "bulk_retry_daemon_connection", "requestBody": { "description": "IDs of the daemons to retry", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Connection retries initiated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "A daemon isn't a ServerPoll daemon; the message names it", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "A daemon was not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/email-install-command": { "post": { "tags": [ "Daemons" ], "summary": "Email install command to current user", "description": "Requires a verified email: the endpoint mails on demand, so an unproven address would let\nanyone send Scanopy mail to an inbox they don't own.\n\nSession-only, and `IsUser` says so at the extractor rather than in the body. \"The current user\"\nhas no answer for an automation identity: a user API key carries `user_id` but no address, so\nthis endpoint could never serve one. An API key that wants the command reads it directly from\n`GET /api/v1/daemons/{id}/install-command`.", "operationId": "email_install_command", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmailInstallCommandRequest" } } }, "required": true }, "responses": { "200": { "description": "Email sent", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Email service not configured, no command for this method, or api key missing or not this daemon's", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "User session or verified email required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/daemons/export/csv": { "get": { "tags": [ "Daemons" ], "summary": "Export Daemons to CSV", "description": "Export all Daemons matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_daemons_csv", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "statuses", "in": "query", "description": "Only daemons in one of these statuses. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonHealth" } } }, { "name": "os", "in": "query", "description": "Only daemons installed on one of these operating systems. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonOs" } } }, { "name": "modes", "in": "query", "description": "Only daemons that connect one of these ways. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonMode" } } }, { "name": "maintainer_ids", "in": "query", "description": "Only daemons maintained by one of these users. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "versions", "in": "query", "description": "Only daemons running one of these versions. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "interfaced_subnet_ids", "in": "query", "description": "Only daemons with an interface on one of these subnets. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns daemons that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the daemon must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and url.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing Daemons", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/field-values/{field}": { "get": { "tags": [ "Daemons" ], "summary": "List the values of a Daemon field", "description": "Every distinct value of one Daemon field across the Daemons the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_daemon_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/DaemonOrderField" } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "statuses", "in": "query", "description": "Only daemons in one of these statuses. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonHealth" } } }, { "name": "os", "in": "query", "description": "Only daemons installed on one of these operating systems. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonOs" } } }, { "name": "modes", "in": "query", "description": "Only daemons that connect one of these ways. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DaemonMode" } } }, { "name": "maintainer_ids", "in": "query", "description": "Only daemons maintained by one of these users. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "versions", "in": "query", "description": "Only daemons running one of these versions. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "interfaced_subnet_ids", "in": "query", "description": "Only daemons with an interface on one of these subnets. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns daemons that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the daemon must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and url.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Distinct values and their Daemon counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/provision": { "post": { "tags": [ "Daemons" ], "summary": "Provision a daemon, or re-provision an existing one", "description": "Creates a daemon record on the server before the daemon is installed and mints an API key\nbound to it 1:1. Returns the daemon record and that key, which is shown only once and must\nbe configured on the daemon.\n\nWhen `daemon_id` is supplied the existing record is reused instead of creating a new one,\ngiving a legacy daemon (one with no bound key) a pathway to a dedicated key without losing\nits host, discovery jobs, or history. Re-provisioning always mints a fresh key.\n\nInstall commands are not built here — fetch them from the install-command endpoint, which\nbuilds them idempotently and fills in the key this returns. That keeps a display-only\nregenerate (an OS switch, an advanced-setting change) from re-minting the key.", "operationId": "provision_daemon", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProvisionDaemonRequest" } } }, "required": true }, "responses": { "201": { "description": "Daemon provisioned successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_ProvisionDaemonResponse" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Forbidden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Daemon is live and already has a bound key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/provision-eligibility": { "get": { "tags": [ "Daemons", "internal" ], "summary": "Check whether a new daemon can be provisioned", "description": "The create wizard reads this before its first step, so a refusal from `POST /provision`\n(no verified owner and a daemon already exists) stops the user up front rather than after\nthey have configured everything. Reads only the caller's own organization.", "operationId": "get_provision_eligibility", "responses": { "200": { "description": "Provisioning eligibility", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_ProvisionEligibility" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/test-reachability": { "post": { "tags": [ "Daemons" ], "summary": "Test reachability of a daemon URL", "description": "Performs a TCP connection test and optionally an HTTP health check\nto verify that a daemon URL is reachable from the server.", "operationId": "test_daemon_reachability", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TestReachabilityRequest" } } }, "required": true }, "responses": { "200": { "description": "Reachability test result", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_TestReachabilityResponse" } } } }, "400": { "description": "Invalid URL", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/{id}": { "get": { "tags": [ "Daemons" ], "summary": "Get daemon by ID", "description": "Returns a specific daemon with computed version status.", "operationId": "get_daemon_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Daemon found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DaemonResponse" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Daemons" ], "summary": "Update daemon", "description": "Edits the server-side daemon record: its name, maintainer, tags, and — for ServerPoll —\nthe url the server dials. Identity and server-managed fields (site, mode, host, key\nbinding, version, liveness) are restored from the existing record by\n`preserve_immutable_fields`.", "operationId": "update_daemon", "parameters": [ { "name": "id", "in": "path", "description": "daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Daemon" } } }, "required": true }, "responses": { "200": { "description": "daemon updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Daemon" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Daemons" ], "summary": "Delete daemon", "operationId": "delete_daemon", "parameters": [ { "name": "id", "in": "path", "description": "daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "daemon deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "daemon has active sessions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/{id}/install-command": { "get": { "tags": [ "Daemons" ], "summary": "Generate daemon install command", "description": "A pure, idempotent builder — it never mints or persists anything. The api key in an `install`\nor `rekey` command is a placeholder (``) the caller substitutes from the plaintext it holds; a\n`reconfigure` command carries no key at all. Minting is a separate mutation\n(`POST /provision`), so regenerating a command here (advanced-setting change, OS switch, the\nDetails reconfigure view) never rotates the daemon's key.\n\nThe server derives the exact command shape from the record: DaemonPoll vs ServerPoll for the\nflags, and — for `rekey` — whether the daemon has checked in (`last_seen`) to decide whether\nthe command names the existing install it re-keys. An `install` command reads the same before\nand after the daemon's first handshake.", "operationId": "get_daemon_install_command", "parameters": [ { "name": "id", "in": "path", "description": "daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "purpose", "in": "query", "description": "`install` or `rekey` (with the api-key placeholder), or `reconfigure` (credential-free).", "required": true, "schema": { "$ref": "#/components/schemas/InstallCommandType" } }, { "name": "log_level", "in": "query", "description": "Log verbosity the daemon should run at (e.g. `info`, `debug`).", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "log_file", "in": "query", "description": "Path the daemon should write its log file to.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "heartbeat_interval", "in": "query", "description": "How often the daemon reports in, in seconds.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int64", "minimum": 0 } }, { "name": "bind_address", "in": "query", "description": "Address and port the daemon should listen on, for server-polled mode.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "allow_self_signed_certs", "in": "query", "description": "Accept a self-signed certificate when connecting back to the server.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "accept_invalid_scan_certs", "in": "query", "description": "Continue scanning targets that present an untrusted certificate.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "interfaces", "in": "query", "description": "Comma-separated interface names.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "credential_refs", "in": "query", "description": "Comma-separated credential/integration tokens (for the docker-compose env).", "required": false, "schema": { "type": [ "string", "null" ] } } ], "responses": { "200": { "description": "Install command", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_InstallArtifacts" } } } }, "404": { "description": "daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/{id}/retry-connection": { "post": { "tags": [ "Daemons" ], "summary": "Retry connection to unreachable daemon", "description": "Resets the is_unreachable flag for a daemon that was marked unreachable\ndue to repeated polling failures. The poller will attempt to contact\nthe daemon again on the next cycle.", "operationId": "retry_daemon_connection", "parameters": [ { "name": "id", "in": "path", "description": "Daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Connection retry initiated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/daemons/{id}/upgrade-command": { "get": { "tags": [ "Daemons" ], "summary": "Generate daemon upgrade commands", "description": "The steps to replace a daemon's binary and restart it, per OS, plus the docker method. Like the\ninstall command, a pure builder over the daemon record.", "operationId": "get_daemon_upgrade_command", "parameters": [ { "name": "id", "in": "path", "description": "daemon ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Upgrade commands", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_UpgradeArtifacts" } } } }, "404": { "description": "daemon not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/dashboard/summary": { "get": { "tags": [ "dashboard", "internal" ], "summary": "Get dashboard summary", "description": "Returns aggregated dashboard data including site metrics, daemon health,\nrecent discoveries, and plan usage.", "operationId": "get_dashboard_summary", "responses": { "200": { "description": "Dashboard summary", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DashboardSummary" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/dependencies": { "get": { "tags": [ "Dependencies" ], "summary": "List all Dependencies", "description": "Returns all dependencies the authenticated user has access to.\nSupports pagination via `limit` and `offset` query parameters,\nand ordering via `group_by`, `order_by`, and `order_direction`.", "operationId": "get_all_dependencies", "parameters": [ { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DependencyOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DependencyOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "List of dependencies", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Dependency" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Dependencies" ], "summary": "Create a new Dependency", "operationId": "create_dependency", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Dependency" } } }, "required": true }, "responses": { "200": { "description": "Dependency created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Dependency" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/dependencies/bulk-delete": { "post": { "tags": [ "Dependencies" ], "summary": "Bulk delete Dependencies", "operationId": "bulk_delete_dependencies", "requestBody": { "description": "Array of Dependency IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Dependencies deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/dependencies/export/csv": { "get": { "tags": [ "Dependencies" ], "summary": "Export Dependencies to CSV", "description": "Export all Dependencies matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_dependencies_csv", "parameters": [ { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DependencyOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DependencyOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "CSV file containing Dependencies", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/dependencies/{id}": { "get": { "tags": [ "Dependencies" ], "summary": "Get Dependency by ID", "operationId": "get_dependency_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Dependency ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Dependency found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Dependency" } } } }, "404": { "description": "Dependency not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Dependencies" ], "summary": "Update a Dependency", "operationId": "update_dependency", "parameters": [ { "name": "id", "in": "path", "description": "Dependency ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Dependency" } } }, "required": true }, "responses": { "200": { "description": "Dependency updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Dependency" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Dependency not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Dependencies" ], "summary": "Delete Dependency", "operationId": "delete_dependency", "parameters": [ { "name": "id", "in": "path", "description": "Dependency ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Dependency deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Dependency not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery": { "get": { "tags": [ "Discoveries" ], "summary": "List discoveries", "description": "Returns discoveries the authenticated user has access to. The run history\ngrows without bound, so this is paginated and ordered server-side rather\nthan filtered in the browser.", "operationId": "get_all_discoveries", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "daemon_ids", "in": "query", "description": "Filter by daemon ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "discovery_types", "in": "query", "description": "Only runs of one of these discovery types.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "phases", "in": "query", "description": "Only runs that ended in one of these phases (`run_type.results.phase`). Repeat for\nseveral. Configurations, which have no recorded outcome, never match.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DiscoveryPhase" } } }, { "name": "historical", "in": "query", "description": "`true` returns only completed runs (the history view), `false` only the\nconfigurations that produce them. Omit for both.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "search", "in": "query", "description": "Free-text search across the discovery's name and the name of the daemon\nthat runs it.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "run_types", "in": "query", "description": "Only scan configurations that run one of these ways (`Scheduled`, `AdHoc`). Repeat for\nseveral.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "legacy", "in": "query", "description": "Only configurations of a legacy type (`true`), a current one (`false`), or both (repeat\nit). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "force_full_scan", "in": "query", "description": "Only configurations whose next run is forced to a full scan (`true`), not (`false`), or\nboth (repeat it). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns discoveries that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the discovery must hold one tag from each of. Each entry is a comma-separated\nlist of tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of discoveries", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Discovery" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Discoveries" ], "summary": "Create new Discovery", "operationId": "create_discovery", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Discovery" } } }, "required": true }, "responses": { "200": { "description": "Discovery created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Discovery" } } } }, "400": { "description": "Can't create historical discovery", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/active-sessions": { "get": { "tags": [ "Discoveries" ], "summary": "Get active Discovery Sessions", "operationId": "get_active_sessions", "responses": { "200": { "description": "List of active discovery sessions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_DiscoveryUpdatePayload" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/bulk-cancel": { "post": { "tags": [ "Discoveries" ], "summary": "Cancel Discovery Sessions", "description": "Cancels the running session of each discovery, given discovery IDs. Sessions still queued\nare cancelled before running ones, so cancelling a running session never starts a queued one\nthat is also being cancelled. The request is refused, and nothing is cancelled, if any\ndiscovery isn't running or its session is just starting on its daemon.", "operationId": "bulk_cancel_sessions", "requestBody": { "description": "IDs of the discoveries whose sessions to cancel", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Sessions cancelled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "A discovery isn't running; the message names it", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "A discovery was not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "A session is starting on its daemon", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/bulk-delete": { "post": { "tags": [ "Discoveries" ], "summary": "Bulk delete discoveries", "operationId": "bulk_delete_discoveries", "requestBody": { "description": "Array of Discovery IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "discoveries deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } }, "409": { "description": "discovery has active session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/bulk-set-enabled": { "post": { "tags": [ "Discoveries" ], "summary": "Turn Discovery Schedules On or Off", "description": "Turns the schedule of each scheduled discovery on or off, and changes nothing else about\nthem. The request is refused, and nothing changes, if any discovery has no schedule or is\nrunning.", "operationId": "bulk_set_enabled", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BulkSetEnabledRequest" } } }, "required": true }, "responses": { "200": { "description": "Schedules updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_Discovery" } } } }, "400": { "description": "A discovery has no schedule; the message names it", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "A discovery was not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "A discovery is running", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/bulk-start-session": { "post": { "tags": [ "Discoveries" ], "summary": "Start Discovery Sessions", "description": "Starts a session for each discovery, as `POST /start-session` does for one. Sessions start\nin turn: on a daemon running several, the first is dispatched and the rest queue behind it.\nThe request is refused, and nothing starts, if any discovery is a past run, a rescan, or\nalready running.", "operationId": "bulk_start_sessions", "requestBody": { "description": "IDs of the discoveries to start", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Sessions started, in the order given", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_DiscoveryUpdatePayload" } } } }, "400": { "description": "A discovery can't be started; the message names it", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "A discovery was not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "A discovery is already running", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/export/csv": { "get": { "tags": [ "Discoveries" ], "summary": "Export Discoveries to CSV", "description": "Export all Discoveries matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_discoveries_csv", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "daemon_ids", "in": "query", "description": "Filter by daemon ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "discovery_types", "in": "query", "description": "Only runs of one of these discovery types.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "phases", "in": "query", "description": "Only runs that ended in one of these phases (`run_type.results.phase`). Repeat for\nseveral. Configurations, which have no recorded outcome, never match.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DiscoveryPhase" } } }, { "name": "historical", "in": "query", "description": "`true` returns only completed runs (the history view), `false` only the\nconfigurations that produce them. Omit for both.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "search", "in": "query", "description": "Free-text search across the discovery's name and the name of the daemon\nthat runs it.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "run_types", "in": "query", "description": "Only scan configurations that run one of these ways (`Scheduled`, `AdHoc`). Repeat for\nseveral.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "legacy", "in": "query", "description": "Only configurations of a legacy type (`true`), a current one (`false`), or both (repeat\nit). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "force_full_scan", "in": "query", "description": "Only configurations whose next run is forced to a full scan (`true`), not (`false`), or\nboth (repeat it). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns discoveries that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the discovery must hold one tag from each of. Each entry is a comma-separated\nlist of tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing Discoveries", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/field-values/{field}": { "get": { "tags": [ "Discoveries" ], "summary": "List the values of a Discovery field", "description": "Every distinct value of one Discovery field across the Discoveries the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_discovery_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/DiscoveryOrderField" } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "daemon_ids", "in": "query", "description": "Filter by daemon ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "discovery_types", "in": "query", "description": "Only runs of one of these discovery types.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "phases", "in": "query", "description": "Only runs that ended in one of these phases (`run_type.results.phase`). Repeat for\nseveral. Configurations, which have no recorded outcome, never match.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DiscoveryPhase" } } }, { "name": "historical", "in": "query", "description": "`true` returns only completed runs (the history view), `false` only the\nconfigurations that produce them. Omit for both.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "search", "in": "query", "description": "Free-text search across the discovery's name and the name of the daemon\nthat runs it.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "run_types", "in": "query", "description": "Only scan configurations that run one of these ways (`Scheduled`, `AdHoc`). Repeat for\nseveral.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "legacy", "in": "query", "description": "Only configurations of a legacy type (`true`), a current one (`false`), or both (repeat\nit). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "force_full_scan", "in": "query", "description": "Only configurations whose next run is forced to a full scan (`true`), not (`false`), or\nboth (repeat it). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns discoveries that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the discovery must hold one tag from each of. Each entry is a comma-separated\nlist of tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Distinct values and their Discovery counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/start-session": { "post": { "tags": [ "Discoveries" ], "summary": "Start a Discovery Session", "operationId": "start_session", "requestBody": { "content": { "text/plain": { "schema": { "type": "string", "format": "uuid" } } }, "required": true }, "responses": { "200": { "description": "Discovery session started", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DiscoveryUpdatePayload" } } } }, "404": { "description": "Discovery not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "A session is already running for this discovery", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/{id}": { "get": { "tags": [ "Discoveries" ], "summary": "Get Discovery by ID", "operationId": "get_discovery_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Discovery ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Discovery found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Discovery" } } } }, "404": { "description": "Discovery not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Discoveries" ], "summary": "Update Discovery", "operationId": "update_discovery", "parameters": [ { "name": "id", "in": "path", "description": "Discovery ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Discovery" } } }, "required": true }, "responses": { "200": { "description": "Discovery updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Discovery" } } } }, "400": { "description": "Can't update historical discovery", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Discoveries" ], "summary": "Delete discovery", "operationId": "delete_discovery", "parameters": [ { "name": "id", "in": "path", "description": "discovery ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "discovery deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "discovery not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "discovery has active session", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/{session_id}/cancel": { "post": { "tags": [ "Discoveries" ], "summary": "Cancel a Discovery Session", "operationId": "cancel_discovery", "parameters": [ { "name": "session_id", "in": "path", "description": "Session ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Discovery session cancelled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/discovery/{session_id}/update": { "post": { "tags": [ "Discoveries", "internal" ], "summary": "Receive discovery progress update from daemon", "description": "Internal endpoint for daemons to report discovery progress.", "operationId": "receive_discovery_update", "parameters": [ { "name": "session_id", "in": "path", "description": "Discovery session ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DiscoveryUpdatePayload" } } }, "required": true }, "responses": { "200": { "description": "Update received", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } }, "security": [ { "daemon_api_key": [] } ] } }, "/api/v1/hosts": { "get": { "tags": [ "Hosts" ], "summary": "List all hosts", "description": "Returns all hosts the authenticated user has access to, with their\nip_addresses, ports, services and interfaces included — pass\n`include_children=false` to omit the ports, services and interfaces and get a\nmuch smaller payload.\nSupports pagination via `limit` and `offset` query parameters, and ordering\nvia `group_by`, `order_by`, and `order_direction`.", "operationId": "get_all_hosts", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_ids", "in": "query", "description": "Filter by the service virtualizing the host. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_names", "in": "query", "description": "Filter by the name of the service virtualizing the host, the value \"Virtualized By\"\ngroups on. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "include_unvirtualized", "in": "query", "description": "`true` also returns hosts nothing virtualizes. Set on its own it returns\nonly those — the \"Not Virtualized\" choice in the UI's filter.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "service_names", "in": "query", "description": "Filter to hosts running a service with one of these names.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sources", "in": "query", "description": "Filter by how the host came to exist (`source.type`). Repeat for several;\n`Inferred` alone lists the hosts only a neighbour advertised.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "manufacturers", "in": "query", "description": "Filter by hardware manufacturer. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "models", "in": "query", "description": "Filter by hardware model. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sys_locations", "in": "query", "description": "Filter by SNMP sysLocation. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "os_families", "in": "query", "description": "Filter by operating system family. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/HostOsFamily" } } }, { "name": "credential_ids", "in": "query", "description": "Filter to hosts assigned one of these credentials. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns hosts that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the host must hold one tag from each of. Each entry is a comma-separated list\nof tag ids; a host matches when it holds at least one tag of every entry. One entry per\ntag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the host's\nname, hostname, sysName, chassis id, serial number, asset tag and\ndescription, and against its IP addresses and the names of services\nrunning on it.\n\nsysName and chassis id are in there because they are rungs of the title\nladder: a host with no name of its own is *shown* under one of them, and\na title you can read but cannot search for is a dead end.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/HostOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/HostOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group, in the group's tag order. Takes\nthe place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only hosts discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the host's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "include_children", "in": "query", "description": "`false` returns hosts with empty `ports`/`services`/`interfaces`.\n`ip_addresses` is always populated: the host's title can come from its\naddress. The children dominate the payload, so callers that only need\nhost identity — name pickers, id→name lookups, counts — should pass\n`false`. Defaults to `true`, so existing callers are unaffected.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "List of hosts with their children", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_HostResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Hosts" ], "summary": "Create a new host", "description": "Creates a host with optional ip_addresses, ports, and services.\nThe `source` field is automatically set to `Manual`.\n\n### Tag Validation\n\n- Tags must exist and belong to your organization\n- Duplicate tag UUIDs are automatically deduplicated\n- Invalid or cross-organization tag UUIDs return a 400 error\n\n", "operationId": "create_host", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateHostRequest" } } }, "required": true }, "responses": { "200": { "description": "Host created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_HostResponse" } } } }, "400": { "description": "Validation error: site not found, subnet mismatch, or invalid tags", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "401": { "description": "No access to the specified site", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] }, { "daemon_api_key": [] } ] } }, "/api/v1/hosts/bulk-delete": { "post": { "tags": [ "Hosts" ], "summary": "Bulk delete hosts", "description": "Deletes multiple hosts in a single request. The request body should be\nan array of host IDs to delete. Fails if any host has an associated daemon.", "operationId": "bulk_delete_hosts", "requestBody": { "description": "Array of Host IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Hosts deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } }, "409": { "description": "One or more hosts has an associated daemon - delete daemons first", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/hosts/bulk-rescan": { "post": { "tags": [ "Hosts" ], "summary": "Rescan hosts", "description": "Rescans several hosts at once. Hosts last found by the same daemon share one\nrescan, which scans all of their addresses in one session; hosts found by\ndifferent daemons get one rescan per daemon. Every host is checked as in\n`POST /{id}/rescan` before any rescan starts, and the request is refused if\nany of them can't be rescanned.\n\nReturns one session per daemon.", "operationId": "bulk_rescan_hosts", "requestBody": { "description": "IDs of the hosts to rescan", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Rescan sessions started, one per daemon", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_DiscoveryUpdatePayload" } } } }, "400": { "description": "A host cannot be rescanned; the message names it", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "A host was not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/hosts/discovery": { "post": { "tags": [ "Hosts", "internal" ], "summary": "Internal endpoint for daemon discovery", "description": "Used by daemons to report discovered hosts. Accepts full entities with\npre-generated IDs. Uses upsert behavior to merge with existing hosts.\n\nTagged as \"internal\" - included in OpenAPI spec for client generation\nbut hidden from public documentation.", "operationId": "create_host_discovery", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DiscoveryHostRequest" } } }, "required": true }, "responses": { "200": { "description": "Host discovered/updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_HostResponse" } } } }, "403": { "description": "Daemon cannot create hosts on other sites", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "daemon_api_key": [] } ] } }, "/api/v1/hosts/export/csv": { "get": { "tags": [ "Hosts" ], "summary": "Export Hosts to CSV", "description": "Export all Hosts matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_hosts_csv", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_ids", "in": "query", "description": "Filter by the service virtualizing the host. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_names", "in": "query", "description": "Filter by the name of the service virtualizing the host, the value \"Virtualized By\"\ngroups on. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "include_unvirtualized", "in": "query", "description": "`true` also returns hosts nothing virtualizes. Set on its own it returns\nonly those — the \"Not Virtualized\" choice in the UI's filter.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "service_names", "in": "query", "description": "Filter to hosts running a service with one of these names.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sources", "in": "query", "description": "Filter by how the host came to exist (`source.type`). Repeat for several;\n`Inferred` alone lists the hosts only a neighbour advertised.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "manufacturers", "in": "query", "description": "Filter by hardware manufacturer. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "models", "in": "query", "description": "Filter by hardware model. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sys_locations", "in": "query", "description": "Filter by SNMP sysLocation. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "os_families", "in": "query", "description": "Filter by operating system family. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/HostOsFamily" } } }, { "name": "credential_ids", "in": "query", "description": "Filter to hosts assigned one of these credentials. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns hosts that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the host must hold one tag from each of. Each entry is a comma-separated list\nof tag ids; a host matches when it holds at least one tag of every entry. One entry per\ntag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the host's\nname, hostname, sysName, chassis id, serial number, asset tag and\ndescription, and against its IP addresses and the names of services\nrunning on it.\n\nsysName and chassis id are in there because they are rungs of the title\nladder: a host with no name of its own is *shown* under one of them, and\na title you can read but cannot search for is a dead end.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/HostOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/HostOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group, in the group's tag order. Takes\nthe place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only hosts discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the host's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "include_children", "in": "query", "description": "`false` returns hosts with empty `ports`/`services`/`interfaces`.\n`ip_addresses` is always populated: the host's title can come from its\naddress. The children dominate the payload, so callers that only need\nhost identity — name pickers, id→name lookups, counts — should pass\n`false`. Defaults to `true`, so existing callers are unaffected.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "CSV file containing Hosts", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/hosts/export/zip": { "get": { "tags": [ "Hosts" ], "summary": "Export hosts with children to ZIP", "description": "Exports all hosts matching the filter criteria along with their children\n(ip_addresses, ports, services, interfaces) as a ZIP archive containing\nseparate CSV files for each entity type.", "operationId": "export_hosts_zip", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_ids", "in": "query", "description": "Filter by the service virtualizing the host. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_names", "in": "query", "description": "Filter by the name of the service virtualizing the host, the value \"Virtualized By\"\ngroups on. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "include_unvirtualized", "in": "query", "description": "`true` also returns hosts nothing virtualizes. Set on its own it returns\nonly those — the \"Not Virtualized\" choice in the UI's filter.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "service_names", "in": "query", "description": "Filter to hosts running a service with one of these names.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sources", "in": "query", "description": "Filter by how the host came to exist (`source.type`). Repeat for several;\n`Inferred` alone lists the hosts only a neighbour advertised.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "manufacturers", "in": "query", "description": "Filter by hardware manufacturer. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "models", "in": "query", "description": "Filter by hardware model. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sys_locations", "in": "query", "description": "Filter by SNMP sysLocation. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "os_families", "in": "query", "description": "Filter by operating system family. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/HostOsFamily" } } }, { "name": "credential_ids", "in": "query", "description": "Filter to hosts assigned one of these credentials. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns hosts that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the host must hold one tag from each of. Each entry is a comma-separated list\nof tag ids; a host matches when it holds at least one tag of every entry. One entry per\ntag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the host's\nname, hostname, sysName, chassis id, serial number, asset tag and\ndescription, and against its IP addresses and the names of services\nrunning on it.\n\nsysName and chassis id are in there because they are rungs of the title\nladder: a host with no name of its own is *shown* under one of them, and\na title you can read but cannot search for is a dead end.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/HostOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/HostOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group, in the group's tag order. Takes\nthe place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only hosts discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the host's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "include_children", "in": "query", "description": "`false` returns hosts with empty `ports`/`services`/`interfaces`.\n`ip_addresses` is always populated: the host's title can come from its\naddress. The children dominate the payload, so callers that only need\nhost identity — name pickers, id→name lookups, counts — should pass\n`false`. Defaults to `true`, so existing callers are unaffected.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "ZIP file containing CSVs", "content": { "application/zip": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/hosts/field-values/{field}": { "get": { "tags": [ "Hosts" ], "summary": "List the values of a Host field", "description": "Every distinct value of one Host field across the Hosts the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_host_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/HostOrderField" } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_ids", "in": "query", "description": "Filter by the service virtualizing the host. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_names", "in": "query", "description": "Filter by the name of the service virtualizing the host, the value \"Virtualized By\"\ngroups on. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "include_unvirtualized", "in": "query", "description": "`true` also returns hosts nothing virtualizes. Set on its own it returns\nonly those — the \"Not Virtualized\" choice in the UI's filter.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "service_names", "in": "query", "description": "Filter to hosts running a service with one of these names.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sources", "in": "query", "description": "Filter by how the host came to exist (`source.type`). Repeat for several;\n`Inferred` alone lists the hosts only a neighbour advertised.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "manufacturers", "in": "query", "description": "Filter by hardware manufacturer. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "models", "in": "query", "description": "Filter by hardware model. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sys_locations", "in": "query", "description": "Filter by SNMP sysLocation. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "os_families", "in": "query", "description": "Filter by operating system family. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/HostOsFamily" } } }, { "name": "credential_ids", "in": "query", "description": "Filter to hosts assigned one of these credentials. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns hosts that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the host must hold one tag from each of. Each entry is a comma-separated list\nof tag ids; a host matches when it holds at least one tag of every entry. One entry per\ntag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the host's\nname, hostname, sysName, chassis id, serial number, asset tag and\ndescription, and against its IP addresses and the names of services\nrunning on it.\n\nsysName and chassis id are in there because they are rungs of the title\nladder: a host with no name of its own is *shown* under one of them, and\na title you can read but cannot search for is a dead end.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/HostOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/HostOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group, in the group's tag order. Takes\nthe place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only hosts discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the host's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "include_children", "in": "query", "description": "`false` returns hosts with empty `ports`/`services`/`interfaces`.\n`ip_addresses` is always populated: the host's title can come from its\naddress. The children dominate the payload, so callers that only need\nhost identity — name pickers, id→name lookups, counts — should pass\n`false`. Defaults to `true`, so existing callers are unaffected.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "Distinct values and their Host counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/hosts/{destination_host}/consolidate/{other_host}": { "put": { "tags": [ "Hosts" ], "summary": "Consolidate hosts", "description": "Merges all ip_addresses, ports, and services from `other_host` into\n`destination_host`, then deletes `other_host`. Both hosts must be\non the same site.\n\n### Merge Behavior\n\n- **Interfaces**: Transferred to destination. If an interface with matching subnet+IP or MAC\n already exists on destination, bindings are remapped to use the existing interface.\n- **Ports**: Transferred to destination. If a port with the same number and protocol already\n exists, bindings are remapped to use the existing port.\n- **Services**: Transferred to destination with deduplication.\n See [upsert behavior](https://scanopy.net/docs/discovery/#upsert-behavior) for details.\n\n### Restrictions\n\n- Cannot consolidate a host with itself.\n- Cannot consolidate a host that has a daemon - consolidate into it instead.", "operationId": "consolidate_hosts", "parameters": [ { "name": "destination_host", "in": "path", "description": "Destination host ID - will receive all children", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "other_host", "in": "path", "description": "Host to merge into destination - will be deleted", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Hosts consolidated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_HostResponse" } } } }, "400": { "description": "Validation error: same host, has daemon, or different sites", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "One or both hosts not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/hosts/{id}": { "get": { "tags": [ "Hosts" ], "summary": "Get a host by ID", "description": "Returns a single host with its ip_addresses, ports, and services.", "operationId": "get_host_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Host ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Host found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_HostResponse" } } } }, "404": { "description": "Host not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Hosts" ], "summary": "Update a host", "description": "Updates host properties. Children (ip_addresses, ports, services, interfaces) are synced\nfrom the fields on this same request body when provided — omit a field to leave that child\nset untouched.\n\n### Tag Validation\n\n- Tags must exist and belong to your organization\n- Duplicate tag UUIDs are automatically deduplicated\n- Invalid or cross-organization tag UUIDs return a 400 error", "operationId": "update_host", "parameters": [ { "name": "id", "in": "path", "description": "Host ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateHostRequest" } } }, "required": true }, "responses": { "200": { "description": "Host updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_HostResponse" } } } }, "400": { "description": "Validation error: invalid tags", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Host not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Hosts" ], "summary": "Delete a host", "description": "Prevents deletion if the host has a daemon associated with it", "operationId": "delete_host", "parameters": [ { "name": "id", "in": "path", "description": "Host ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Host deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Host not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Host has associated daemon", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/hosts/{id}/rescan": { "post": { "tags": [ "Hosts" ], "summary": "Rescan a host", "description": "Starts a one-shot scan of this host's addresses and nothing else, answering\n\"is this host still there, and is its data current?\" without sweeping the\nwhole subnet.\n\nThe scan runs on the daemon that last discovered this host, which is evidence\nit can reach the address. Where the daemon has an interface with a MAC on the\naddress's subnet it ARPs the target, which sees a live host even when every\nport is firewalled. Every other address, on a MAC-less interface (a\npoint-to-point tunnel) or on a subnet the daemon reaches through a router, is\nprobed by TCP. An address that doesn't answer comes back as not found. A\nloopback address is reached locally and is excluded from the target set.\n\nDaemons at or below `last_without_routed_rescan` only rescan addresses on\nsubnets they have an interface on, and the request is refused when the host\nhas none.\n\nReturns the session, which streams progress over `/api/v1/discovery/stream`\nlike any other scan. A `Queued` phase means the daemon is busy; it will start\nwhen the running scan finishes.", "operationId": "rescan_host", "parameters": [ { "name": "id", "in": "path", "description": "Host ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Rescan session started", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DiscoveryUpdatePayload" } } } }, "400": { "description": "Host cannot be rescanned (never scanned, daemon gone, or daemon too old)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Host not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/interfaces": { "get": { "tags": [ "Interfaces" ], "summary": "List all Interfaces", "operationId": "list_interfaces", "parameters": [ { "name": "host_id", "in": "query", "description": "Filter by host ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "List of Interfaces", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Interface" }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Interfaces" ], "summary": "Create a new Interface", "description": "Creates an SNMP ifTable entry for a host. These are typically created by\nSNMP discovery, but can also be created manually.", "operationId": "create_interface", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Interface" } } }, "required": true }, "responses": { "200": { "description": "If entry created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Interface" } } } }, "400": { "description": "Site mismatch or duplicate if_index", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/interfaces/bulk-delete": { "post": { "tags": [ "Interfaces" ], "summary": "Bulk delete Interfaces", "operationId": "bulk_delete_interfaces", "requestBody": { "description": "Array of Interface IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Interfaces deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/interfaces/export/csv": { "get": { "tags": [ "Interfaces" ], "summary": "Export Interfaces to CSV", "description": "Export all Interfaces matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_interfaces_csv", "parameters": [ { "name": "host_id", "in": "query", "description": "Filter by host ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "CSV file containing Interfaces", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/interfaces/{id}": { "get": { "tags": [ "Interfaces" ], "summary": "Get Interface by ID", "operationId": "get_interface_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Interface ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Interface found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Interface" } } } }, "404": { "description": "Interface not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Interfaces" ], "summary": "Update an Interface", "operationId": "update_interface", "parameters": [ { "name": "id", "in": "path", "description": "If entry ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Interface" } } }, "required": true }, "responses": { "200": { "description": "If entry updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Interface" } } } }, "400": { "description": "Site mismatch or invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "If entry not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Interfaces" ], "summary": "Delete Interface", "operationId": "delete_interface", "parameters": [ { "name": "id", "in": "path", "description": "Interface ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Interface deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Interface not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/interfaces/{id}/neighbours": { "get": { "tags": [ "Interfaces" ], "summary": "Get an Interface's neighbours", "description": "Returns the adjacencies resolved for the interface and the raw LLDP/CDP evidence behind them,\nincluding records that resolved to nothing.", "operationId": "get_interface_neighbours", "parameters": [ { "name": "id", "in": "path", "description": "If entry ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Resolved neighbours and raw evidence", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_InterfaceNeighbours" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "If entry not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/invites": { "get": { "tags": [ "Invites" ], "summary": "List all invites", "operationId": "get_invites", "responses": { "200": { "description": "List of active invites", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_Invite" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Invites" ], "summary": "Create invite", "operationId": "create_invite", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateInviteRequest" } } }, "required": true }, "responses": { "200": { "description": "Invite created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Invite" } } } }, "400": { "description": "Recipient named but the caller has no address to send from", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Cannot create invite with higher permissions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/invites/bulk-revoke": { "post": { "tags": [ "Invites" ], "summary": "Revoke invites", "description": "Revokes each invite, as `DELETE /{id}/revoke` does for one. The request is refused, and\nnothing is revoked, if any invite is invalid or one the caller can't revoke.", "operationId": "bulk_revoke_invites", "requestBody": { "description": "IDs of the invites to revoke", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Invites revoked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "An invite is invalid", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "An invite can't be revoked by the caller", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/invites/{id}": { "get": { "tags": [ "Invites" ], "summary": "Get an invite by ID", "operationId": "get_invite", "parameters": [ { "name": "id", "in": "path", "description": "Invite ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Invite details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Invite" } } } }, "400": { "description": "Invalid or expired invite", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/invites/{id}/revoke": { "delete": { "tags": [ "Invites" ], "summary": "Revoke an invite", "operationId": "revoke_invite", "parameters": [ { "name": "id", "in": "path", "description": "Invite ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Invite revoked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Invalid invite", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Cannot revoke this invite", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/ip-addresses": { "get": { "tags": [ "IP Addresses" ], "summary": "List all IP Addresses", "operationId": "list_ip_addresses", "parameters": [ { "name": "host_id", "in": "query", "description": "Filter by host ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "subnet_id", "in": "query", "description": "Filter by subnet ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "List of IP Addresses", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "IP Addresses" ], "summary": "Create a new IP address\nPosition is automatically assigned to the end of the host's IP address list.", "operationId": "create_ip_address", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IPAddress" } } }, "required": true }, "responses": { "200": { "description": "IP address created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_IPAddress" } } } }, "400": { "description": "Site mismatch or invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/ip-addresses/bulk-delete": { "post": { "tags": [ "IP Addresses" ], "summary": "Bulk delete IP addresses\nRemaining IP addresses for affected hosts are renumbered to maintain sequential positions.", "operationId": "bulk_delete_ip_addresses", "requestBody": { "description": "Array of IP Address IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "IP addresses deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } }, "400": { "description": "No IDs provided", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/ip-addresses/export/csv": { "get": { "tags": [ "IP Addresses" ], "summary": "Export IP Addresses to CSV", "description": "Export all IP Addresses matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_ip_addresses_csv", "parameters": [ { "name": "host_id", "in": "query", "description": "Filter by host ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "subnet_id", "in": "query", "description": "Filter by subnet ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "CSV file containing IP Addresses", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/ip-addresses/{id}": { "get": { "tags": [ "IP Addresses" ], "summary": "Get IP Address by ID", "operationId": "get_ip_address_by_id", "parameters": [ { "name": "id", "in": "path", "description": "IP Address ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "IP Address found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_IPAddress" } } } }, "404": { "description": "IP Address not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "IP Addresses" ], "summary": "Update an IP address\nPosition must be within valid range and not conflict with other IP addresses.", "operationId": "update_ip_address", "parameters": [ { "name": "id", "in": "path", "description": "IP address ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IPAddress" } } }, "required": true }, "responses": { "200": { "description": "IP address updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_IPAddress" } } } }, "400": { "description": "Site mismatch or invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "IP address not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "IP Addresses" ], "summary": "Delete an IP address\nRemaining IP addresses for the host are renumbered to maintain sequential positions.", "operationId": "delete_ip_address", "parameters": [ { "name": "id", "in": "path", "description": "IP address ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "IP address deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "IP address not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/licenses/entitlement": { "post": { "tags": [ "billing", "internal" ], "summary": "Exchange an online license key for an entitlement", "description": "Called by self-hosted Scanopy servers. The key is the credential, so the\nendpoint takes no session or API key. Every success records the check-in\nand returns an entitlement minted from the organization's current plan and\npaid-through date.", "operationId": "get_entitlement", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EntitlementRequest" } } }, "required": true }, "responses": { "200": { "description": "Entitlement for the key's organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_EntitlementResponse" } } } }, "400": { "description": "Malformed key, bad signature, or not an online key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Key was regenerated, the organization is not on a self-hosted plan, or the key already runs on as many servers as the plan covers", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "429": { "description": "Too many requests for this organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/v1/licenses/instances": { "get": { "tags": [ "billing", "internal" ], "summary": "List the server instances running this organization's license key", "description": "An instance counts against the limit while it has checked in within the\nlast week.", "operationId": "list_license_instances", "responses": { "200": { "description": "The organization's server instances", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_LicenseInstancesResponse" } } } }, "403": { "description": "Not an owner", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/licenses/instances/{id}": { "delete": { "tags": [ "billing", "internal" ], "summary": "Delete a server instance from this organization's license key", "description": "Frees its slot. A server that is still running is listed again at its next\ncheck-in; rotating the key is what stops one.", "operationId": "delete_license_instance", "parameters": [ { "name": "id", "in": "path", "description": "License instance ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Instance deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "403": { "description": "Not an owner", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "No such instance in this organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/licenses/keys": { "post": { "tags": [ "billing", "internal" ], "summary": "Mint a license key for this organization's self-hosted servers", "description": "Online keys work on any plan with a self-hosted license. Offline keys need\na plan with air-gapped deployment.", "operationId": "create_license_key", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateLicenseKeyRequest" } } }, "required": true }, "responses": { "200": { "description": "Signed license key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_LicenseKeyResponse" } } } }, "403": { "description": "Not an owner, or the plan does not include this key type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/licenses/keys/current": { "get": { "tags": [ "billing", "internal" ], "summary": "Read this organization's current license key", "description": "Returns whichever key type the organization has issued, minted from its\ncurrent state. Online keys are deterministic, so this returns the same\nstring every time until the key is regenerated or the type is switched.", "operationId": "get_current_license_key", "responses": { "200": { "description": "The organization's current license key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_LicenseKeyResponse" } } } }, "403": { "description": "Not an owner, or the organization has no license", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/licenses/keys/rotate": { "post": { "tags": [ "billing", "internal" ], "summary": "Rotate this organization's license key", "description": "Retires every key issued so far: a server still using an online key gets\n403 from the entitlement endpoint and needs the new key.", "operationId": "rotate_license_key", "responses": { "200": { "description": "Key rotated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "403": { "description": "Not an owner", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/organizations": { "get": { "tags": [ "Organizations" ], "summary": "Get the current user's organization", "operationId": "get_organization", "responses": { "200": { "description": "Organization details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Organization" } } } }, "404": { "description": "Organization not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/organizations/daemon-prompt-response": { "post": { "tags": [ "Organizations" ], "summary": "Record the user's response to the daemon-install prompt so it is not shown again.\nEach CTA records a distinct onboarding milestone, which the UI reads to hide the prompt.\nEvery response is published; the PostHog subscriber turns these into funnel events, so\nno client-side telemetry is needed.", "operationId": "daemon_prompt_response", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DaemonPromptResponseRequest" } } }, "required": true }, "responses": { "200": { "description": "Response recorded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } } } }, "/api/v1/organizations/profile": { "post": { "tags": [ "Organizations" ], "summary": "Update user profile with deferred marketing fields", "operationId": "update_profile", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProfileUpdateRequest" } } }, "required": true }, "responses": { "200": { "description": "Profile updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } } } }, "/api/v1/organizations/referral-source": { "post": { "tags": [ "Organizations" ], "summary": "Submit referral source (how did you hear about us)", "operationId": "submit_referral_source", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReferralSourceRequest" } } }, "required": true }, "responses": { "200": { "description": "Referral source recorded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } } } }, "/api/v1/organizations/{id}": { "put": { "tags": [ "Organizations" ], "summary": "Update organization name", "operationId": "update_org_name", "parameters": [ { "name": "id", "in": "path", "description": "Organization ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "text/plain": { "schema": { "type": "string" } } }, "required": true }, "responses": { "200": { "description": "Organization updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Organization" } } } }, "403": { "description": "Only owners can update organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Organization not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Organizations" ], "summary": "Delete the organization entirely, including all data and users", "operationId": "delete_organization", "parameters": [ { "name": "id", "in": "path", "description": "Organization ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Organization deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "403": { "description": "Cannot delete another organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Organization not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/organizations/{id}/populate-demo": { "post": { "tags": [ "Organizations", "internal" ], "summary": "Populate demo data (only available for demo organizations).", "description": "Runs the population off the request thread (a `tokio::spawn`) and returns\n`202` immediately — the work is a few hundred sequential DB round-trips and\nwould otherwise exceed the reverse-proxy request timeout against a remote\ndatabase. Poll `GET /{id}/populate-demo/status` for completion/failure.", "operationId": "populate_demo_data", "parameters": [ { "name": "id", "in": "path", "description": "Organization ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "202": { "description": "Demo data population started", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DemoPopulateStatus" } } } }, "403": { "description": "Only available for demo organizations", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Organization not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Population already in progress", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/organizations/{id}/populate-demo/status": { "get": { "tags": [ "Organizations", "internal" ], "summary": "Poll the status of an org's background demo-populate task.", "operationId": "populate_demo_status", "parameters": [ { "name": "id", "in": "path", "description": "Organization ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Demo populate status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_DemoPopulateStatus" } } } }, "404": { "description": "No demo-populate task for this organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/organizations/{id}/reset": { "post": { "tags": [ "Organizations", "internal" ], "summary": "Reset all organization data (delete all entities except organization and owner user)", "operationId": "reset", "parameters": [ { "name": "id", "in": "path", "description": "Organization ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Organization reset", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "403": { "description": "Cannot reset another organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Organization not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/ports": { "get": { "tags": [ "Ports" ], "summary": "List all Ports", "operationId": "list_ports", "parameters": [ { "name": "host_id", "in": "query", "description": "Filter by host ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "List of Ports", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Port" }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Ports" ], "summary": "Create a new port", "operationId": "create_port", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Port" } } }, "required": true }, "responses": { "200": { "description": "Port created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Port" } } } }, "400": { "description": "Site mismatch or duplicate port", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/ports/bulk-delete": { "post": { "tags": [ "Ports" ], "summary": "Bulk delete Ports", "operationId": "bulk_delete_ports", "requestBody": { "description": "Array of Port IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Ports deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/ports/export/csv": { "get": { "tags": [ "Ports" ], "summary": "Export Ports to CSV", "description": "Export all Ports matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_ports_csv", "parameters": [ { "name": "host_id", "in": "query", "description": "Filter by host ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "CSV file containing Ports", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/ports/{id}": { "get": { "tags": [ "Ports" ], "summary": "Get Port by ID", "operationId": "get_port_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Port ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Port found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Port" } } } }, "404": { "description": "Port not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Ports" ], "summary": "Update a port", "operationId": "update_port", "parameters": [ { "name": "id", "in": "path", "description": "Port ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Port" } } }, "required": true }, "responses": { "200": { "description": "Port updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Port" } } } }, "400": { "description": "Site mismatch or invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Port not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Ports" ], "summary": "Delete Port", "operationId": "delete_port", "parameters": [ { "name": "id", "in": "path", "description": "Port ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Port deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Port not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/release-notes/seen": { "post": { "tags": [ "release-notes" ], "summary": "Dismiss release notes", "description": "Marks every release up to the version this server runs as seen by the\ncaller, on every device.", "operationId": "mark_release_notes_seen", "responses": { "200": { "description": "Dismissed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/release-notes/unseen": { "get": { "tags": [ "release-notes" ], "summary": "List unseen release notes", "description": "The releases newer than the last one the caller dismissed, up to the\nversion this server runs.", "operationId": "get_unseen_release_notes", "responses": { "200": { "description": "Unseen releases", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_UnseenReleaseNotes" } } } }, "401": { "description": "Not signed in", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] } }, "/api/v1/search": { "get": { "tags": [ "search", "internal" ], "summary": "Search every entity type", "description": "Returns a page of matches of each entity type the caller can list, on the sites and in the\norganization they can access, with each type's total. Each type matches the text against its\nown fields; tags narrow every type to entities carrying all of them. Name `entity_type` with\n`offset` to page through one type.", "operationId": "search", "parameters": [ { "name": "q", "in": "query", "description": "Free text. Case-insensitive substring match against each entity type's searchable fields.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "tag_ids", "in": "query", "description": "Only entities carrying every one of these tags. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "entity_type", "in": "query", "description": "Search only this entity type, for its later pages. Omit for the first page of every type.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/EntityDiscriminants" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Matches per type (1-50, default 5).", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 50, "minimum": 1 } }, { "name": "offset", "in": "query", "description": "Matches to skip in each type.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Matches grouped by entity type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_GlobalSearchResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/services": { "get": { "tags": [ "Services" ], "summary": "List all services", "description": "Returns all services the authenticated user has access to.\nSupports pagination via `limit` and `offset` query parameters,\nand ordering via `group_by`, `order_by`, and `order_direction`.", "operationId": "get_all_services", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "host_ids", "in": "query", "description": "Filter by host ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "service_definitions", "in": "query", "description": "Only services with one of these definitions.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "virtualization_service_ids", "in": "query", "description": "Filter by the service containerizing this one. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_names", "in": "query", "description": "Filter by the name of the service containerizing this one, the value \"Containerized\"\ngroups on. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "include_uncontainerized", "in": "query", "description": "`true` also returns services nothing containerizes. Set on its own it\nreturns only those — the \"Not Containerized\" choice in the UI's filter.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "sources", "in": "query", "description": "Filter by how the service came to exist (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "match_confidences", "in": "query", "description": "Filter by match confidence (`source.details.confidence`). Repeat for several. Only\nservices discovery matched to a definition carry one.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/MatchConfidence" } } }, { "name": "guest_states", "in": "query", "description": "Filter by the run state of the container a service is, at the last scan. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/GuestState" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns services that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the service must hold one tag from each of. Each entry is a comma-separated\nlist of tag ids; a service matches when it holds at least one tag of every entry. One\nentry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the service's\nname and definition, and against the name of the host it runs on.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/ServiceOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/ServiceOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group, in the group's tag order. Takes\nthe place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "ports", "in": "query", "description": "Only services exposed on one of these port numbers, over either protocol.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "integer", "format": "int32", "minimum": 0 } } }, { "name": "exclude_categories", "in": "query", "description": "Exclude services belonging to these categories.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/ServiceCategory" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only services discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the service's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "List of services", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Service" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Services" ], "summary": "Create a new service", "description": "Creates a service with optional bindings to ip_addresses or ports.\nThe `id`, `created_at`, `updated_at`, and `source` fields are generated server-side.\nBindings are specified without `service_id` or `site_id` - these are assigned automatically.\n\n### Binding Validation Rules\n\n- **Cross-host validation**: All bindings must reference ports/interfaces that belong to the\n service's host. Bindings referencing entities from other hosts will be rejected.\n- **Deduplication**: Duplicate bindings in the same request are automatically deduplicated.\n- **All-interfaces precedence**: If a port binding with `ip_address_id: null` (all ip_addresses)\n is included, any specific-interface bindings for the same port are automatically removed.\n- **Conflict detection**: Interface bindings conflict with port bindings on the same interface.\n A port binding on all ip_addresses conflicts with any interface binding.", "operationId": "create_service", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateServiceRequest" } } }, "required": true }, "responses": { "200": { "description": "Service created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Service" } } } }, "400": { "description": "Validation error: host site mismatch, cross-host binding, or binding conflict", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/services/bulk-delete": { "post": { "tags": [ "Services" ], "summary": "Bulk delete Services", "operationId": "bulk_delete_services", "requestBody": { "description": "Array of Service IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Services deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/services/export/csv": { "get": { "tags": [ "Services" ], "summary": "Export Services to CSV", "description": "Export all Services matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_services_csv", "parameters": [ { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "host_ids", "in": "query", "description": "Filter by host ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "service_definitions", "in": "query", "description": "Only services with one of these definitions.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "virtualization_service_ids", "in": "query", "description": "Filter by the service containerizing this one. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_names", "in": "query", "description": "Filter by the name of the service containerizing this one, the value \"Containerized\"\ngroups on. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "include_uncontainerized", "in": "query", "description": "`true` also returns services nothing containerizes. Set on its own it\nreturns only those — the \"Not Containerized\" choice in the UI's filter.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "sources", "in": "query", "description": "Filter by how the service came to exist (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "match_confidences", "in": "query", "description": "Filter by match confidence (`source.details.confidence`). Repeat for several. Only\nservices discovery matched to a definition carry one.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/MatchConfidence" } } }, { "name": "guest_states", "in": "query", "description": "Filter by the run state of the container a service is, at the last scan. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/GuestState" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns services that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the service must hold one tag from each of. Each entry is a comma-separated\nlist of tag ids; a service matches when it holds at least one tag of every entry. One\nentry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the service's\nname and definition, and against the name of the host it runs on.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/ServiceOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/ServiceOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group, in the group's tag order. Takes\nthe place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "ports", "in": "query", "description": "Only services exposed on one of these port numbers, over either protocol.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "integer", "format": "int32", "minimum": 0 } } }, { "name": "exclude_categories", "in": "query", "description": "Exclude services belonging to these categories.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/ServiceCategory" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only services discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the service's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "CSV file containing Services", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/services/field-values/{field}": { "get": { "tags": [ "Services" ], "summary": "List the values of a Service field", "description": "Every distinct value of one Service field across the Services the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_service_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/ServiceOrderField" } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "host_ids", "in": "query", "description": "Filter by host ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "service_definitions", "in": "query", "description": "Only services with one of these definitions.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "virtualization_service_ids", "in": "query", "description": "Filter by the service containerizing this one. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "virtualization_service_names", "in": "query", "description": "Filter by the name of the service containerizing this one, the value \"Containerized\"\ngroups on. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "include_uncontainerized", "in": "query", "description": "`true` also returns services nothing containerizes. Set on its own it\nreturns only those — the \"Not Containerized\" choice in the UI's filter.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "sources", "in": "query", "description": "Filter by how the service came to exist (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "match_confidences", "in": "query", "description": "Filter by match confidence (`source.details.confidence`). Repeat for several. Only\nservices discovery matched to a definition carry one.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/MatchConfidence" } } }, { "name": "guest_states", "in": "query", "description": "Filter by the run state of the container a service is, at the last scan. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/GuestState" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns services that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the service must hold one tag from each of. Each entry is a comma-separated\nlist of tag ids; a service matches when it holds at least one tag of every entry. One\nentry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the service's\nname and definition, and against the name of the host it runs on.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/ServiceOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/ServiceOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group, in the group's tag order. Takes\nthe place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "ports", "in": "query", "description": "Only services exposed on one of these port numbers, over either protocol.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "integer", "format": "int32", "minimum": 0 } } }, { "name": "exclude_categories", "in": "query", "description": "Exclude services belonging to these categories.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/ServiceCategory" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only services discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the service's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "Distinct values and their Service counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/services/{id}": { "get": { "tags": [ "Services" ], "summary": "Get Service by ID", "operationId": "get_service_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Service ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Service found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Service" } } } }, "404": { "description": "Service not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Services" ], "summary": "Update a service", "description": "Updates an existing service. All binding validation rules from service creation apply here as well.\n\n## Binding Validation Rules\n\n- **Cross-host validation**: All bindings must reference ports/interfaces that belong to the\n service's host. Bindings referencing entities from other hosts will be rejected.\n- **Deduplication**: Duplicate bindings are automatically deduplicated.\n- **All-interfaces precedence**: If a port binding with `ip_address_id: null` (all ip_addresses)\n is included, any specific-interface bindings for the same port are automatically removed.\n- **Conflict detection**: Interface bindings conflict with port bindings on the same interface.", "operationId": "update_service", "parameters": [ { "name": "id", "in": "path", "description": "Service ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Service" } } }, "required": true }, "responses": { "200": { "description": "Service updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Service" } } } }, "400": { "description": "Validation error: host site mismatch, cross-host binding, or binding conflict", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Service not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Services" ], "summary": "Delete Service", "operationId": "delete_service", "parameters": [ { "name": "id", "in": "path", "description": "Service ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Service deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Service not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/shares": { "get": { "tags": [ "Shares" ], "summary": "List all Shares", "operationId": "list_shares", "parameters": [ { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "topology_id", "in": "query", "description": "Filter by topology ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of Shares", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Share" }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Shares" ], "summary": "Create a new share", "operationId": "create_share", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateUpdateShareRequest" } } }, "required": true }, "responses": { "200": { "description": "Share created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Share" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/shares/bulk-delete": { "post": { "tags": [ "Shares" ], "summary": "Bulk delete Shares", "operationId": "bulk_delete_shares", "requestBody": { "description": "Array of Share IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Shares deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/shares/export/csv": { "get": { "tags": [ "Shares" ], "summary": "Export Shares to CSV", "description": "Export all Shares matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_shares_csv", "parameters": [ { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "topology_id", "in": "query", "description": "Filter by topology ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing Shares", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/shares/public/{id}": { "get": { "tags": [ "Shares" ], "summary": "Get share metadata", "description": "Does not include any topology data", "operationId": "get_public_share_metadata", "parameters": [ { "name": "id", "in": "path", "description": "Share ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Share metadata", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_PublicShareMetadata" } } } }, "403": { "description": "Share is disabled or expired", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Share not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/v1/shares/public/{id}/topology": { "post": { "tags": [ "Shares", "internal" ], "summary": "Get topology data for a public share", "operationId": "get_share_topology", "parameters": [ { "name": "id", "in": "path", "description": "Share ID", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "embed", "in": "query", "description": "Return the share prepared for embedding in another page.", "required": false, "schema": { "type": "boolean" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShareTopologyRequest" } } }, "required": true }, "responses": { "200": { "description": "Share metadata and topology data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_ShareWithTopology" } } } }, "400": { "description": "View is not enabled on this share", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "401": { "description": "Access token missing, expired or tampered with", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "402": { "description": "Embedding is not included in the share owner's plan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Share is disabled or expired, or its password was not verified", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Share not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "429": { "description": "Too many requests for this share", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/v1/shares/public/{id}/verify": { "post": { "tags": [ "Shares", "internal" ], "summary": "Verify password for a password-protected share and return an access token.", "description": "The returned token is an HS256 JWT tied to the share's current password\nhash; subsequent `/topology` calls send the token instead of the raw\npassword. Changing the share password invalidates outstanding tokens.", "operationId": "verify_share_password", "parameters": [ { "name": "id", "in": "path", "description": "Share ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "text/plain": { "schema": { "type": "string" } } }, "required": true }, "responses": { "200": { "description": "Password verified; access token issued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_ShareAccessTokenResponse" } } } }, "400": { "description": "Share has no password to verify", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "401": { "description": "Invalid password", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Share is disabled or expired", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Share not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "429": { "description": "Too many attempts for this share", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } } } }, "/api/v1/shares/{id}": { "get": { "tags": [ "Shares" ], "summary": "Get Share by ID", "operationId": "get_share_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Share ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Share found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Share" } } } }, "404": { "description": "Share not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Shares" ], "summary": "Update a share", "operationId": "update_share", "parameters": [ { "name": "id", "in": "path", "description": "Share ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateUpdateShareRequest" } } }, "required": true }, "responses": { "200": { "description": "Share updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Share" } } } }, "404": { "description": "Share not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Shares" ], "summary": "Delete Share", "operationId": "delete_share", "parameters": [ { "name": "id", "in": "path", "description": "Share ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Share deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Share not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/sites": { "get": { "tags": [ "Sites" ], "summary": "List all sites", "operationId": "get_all_sites", "parameters": [ { "name": "credential_ids", "in": "query", "description": "Only sites with one of these credentials assigned. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "effective_stale_after_hours", "in": "query", "description": "Only sites whose effective staleness window is one of these, in hours. Repeat for\nseveral.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "integer", "format": "int64" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns sites that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the site must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the site's name and the names\nof its daemons, subnets and VLANs.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SiteOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SiteOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of sites", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/SiteBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "effective_stale_after_hours": { "type": "integer", "format": "int64", "description": "`stale_after_hours` with the server's default already applied.\n\nComputed, never stored (excluded from `to_params`). Published so the\nfrontend derives staleness from the *same* number the digest uses rather\nthan re-declaring the default in TypeScript, where the two could drift\nand a host could read stale in the app but current in the digest email.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "example": { "created_at": "2026-01-15T10:30:00Z", "credential_ids": [], "effective_stale_after_hours": 672, "id": "550e8400-e29b-41d4-a716-446655440002", "name": "Home Site", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "stale_after_hours": null, "tags": [], "updated_at": "2026-01-15T10:30:00Z" } }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Sites" ], "summary": "Create a new site", "operationId": "create_site", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Site" } } }, "required": true }, "responses": { "200": { "description": "Site created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Site" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/sites/bulk-delete": { "post": { "tags": [ "Sites" ], "summary": "Bulk delete sites", "operationId": "bulk_delete_sites", "requestBody": { "description": "Array of Site IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Sites deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } }, "403": { "description": "User not admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/sites/export/csv": { "get": { "tags": [ "Sites" ], "summary": "Export Sites to CSV", "description": "Export all Sites matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_sites_csv", "parameters": [ { "name": "credential_ids", "in": "query", "description": "Only sites with one of these credentials assigned. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "effective_stale_after_hours", "in": "query", "description": "Only sites whose effective staleness window is one of these, in hours. Repeat for\nseveral.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "integer", "format": "int64" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns sites that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the site must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the site's name and the names\nof its daemons, subnets and VLANs.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SiteOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SiteOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing Sites", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/sites/field-values/{field}": { "get": { "tags": [ "Sites" ], "summary": "List the values of a Site field", "description": "Every distinct value of one Site field across the Sites the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_site_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/SiteOrderField" } }, { "name": "credential_ids", "in": "query", "description": "Only sites with one of these credentials assigned. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "effective_stale_after_hours", "in": "query", "description": "Only sites whose effective staleness window is one of these, in hours. Repeat for\nseveral.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "integer", "format": "int64" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns sites that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the site must hold one tag from each of. Each entry is a comma-separated list of\ntag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the site's name and the names\nof its daemons, subnets and VLANs.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SiteOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SiteOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Distinct values and their Site counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/sites/{id}": { "get": { "tags": [ "Sites" ], "summary": "Get a site by ID", "operationId": "get_by_id_site", "parameters": [ { "name": "id", "in": "path", "description": "Site ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Site found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Site" } } } }, "404": { "description": "Site not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Sites" ], "summary": "Update a site", "operationId": "update_site", "parameters": [ { "name": "id", "in": "path", "description": "Site ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Site" } } }, "required": true }, "responses": { "200": { "description": "Site updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Site" } } } }, "403": { "description": "User not admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Site not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Sites" ], "summary": "Delete a site", "operationId": "delete_site", "parameters": [ { "name": "id", "in": "path", "description": "Site ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Site deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "403": { "description": "User not admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Site not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/snapshots": { "get": { "tags": [ "Snapshots" ], "summary": "List all Snapshots", "operationId": "list_snapshots", "parameters": [ { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of Snapshots", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Snapshot" }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Snapshots" ], "summary": "Take a snapshot of the current live topology + entity state for a site.\nAcquires the discovery snapshot lock, creates the snapshots row, runs\nclose-and-clone to stamp every Snapshotable entity row with `snapshot_id`\nand close them. The topology subscriber inserts the snapshot's topology\nrow off the back of the `Snapshot::Created` event.", "operationId": "create_snapshot", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSnapshotRequest" } } }, "required": true }, "responses": { "200": { "description": "Snapshot created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Snapshot" } } } }, "402": { "description": "Snapshots not available on plan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Site is busy with discovery; retry shortly", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/snapshots/{id}": { "get": { "tags": [ "Snapshots" ], "summary": "Get Snapshot by ID", "operationId": "get_snapshot_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Snapshot ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Snapshot found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Snapshot" } } } }, "404": { "description": "Snapshot not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Snapshots" ], "summary": "Delete Snapshot", "operationId": "delete_snapshot", "parameters": [ { "name": "id", "in": "path", "description": "Snapshot ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Snapshot deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Snapshot not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/subnets": { "get": { "tags": [ "Subnets" ], "summary": "List all subnets", "description": "Returns all subnets accessible to the authenticated user or daemon.\nDaemons can only access subnets within their assigned site.\nSupports pagination via `limit` and `offset` query parameters,\nand ordering via `group_by`, `order_by`, and `order_direction`.", "operationId": "list_subnets", "parameters": [ { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "subnet_types", "in": "query", "description": "Only subnets of one of these types. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sources", "in": "query", "description": "Only subnets that came to exist one of these ways (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "managed_by", "in": "query", "description": "Only bridge subnets created by one of these container runtimes. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "user_managed", "in": "query", "description": "`true` returns only the subnets the user curates, leaving out the ones Scanopy fabricates\n(`Subnet::is_user_managed`).", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns subnets that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the subnet must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SubnetOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SubnetOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only subnets discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the subnet's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the subnet's\nname, CIDR and description.", "required": false, "schema": { "type": [ "string", "null" ] } } ], "responses": { "200": { "description": "List of subnets", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_SubnetResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] }, { "daemon_api_key": [] } ] }, "post": { "tags": [ "Subnets" ], "summary": "Create a new subnet", "operationId": "create_subnet", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Subnet" } } }, "required": true }, "responses": { "200": { "description": "Subnet created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Subnet" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] }, { "daemon_api_key": [] } ] } }, "/api/v1/subnets/bulk-delete": { "post": { "tags": [ "Subnets" ], "summary": "Bulk delete Subnets", "operationId": "bulk_delete_subnets", "requestBody": { "description": "Array of Subnet IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Subnets deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/subnets/export/csv": { "get": { "tags": [ "Subnets" ], "summary": "Export Subnets to CSV", "description": "Export all Subnets matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_subnets_csv", "parameters": [ { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "subnet_types", "in": "query", "description": "Only subnets of one of these types. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sources", "in": "query", "description": "Only subnets that came to exist one of these ways (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "managed_by", "in": "query", "description": "Only bridge subnets created by one of these container runtimes. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "user_managed", "in": "query", "description": "`true` returns only the subnets the user curates, leaving out the ones Scanopy fabricates\n(`Subnet::is_user_managed`).", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns subnets that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the subnet must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SubnetOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SubnetOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only subnets discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the subnet's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the subnet's\nname, CIDR and description.", "required": false, "schema": { "type": [ "string", "null" ] } } ], "responses": { "200": { "description": "CSV file containing Subnets", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/subnets/field-values/{field}": { "get": { "tags": [ "Subnets" ], "summary": "List the values of a Subnet field", "description": "Every distinct value of one Subnet field across the Subnets the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_subnet_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/SubnetOrderField" } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "subnet_types", "in": "query", "description": "Only subnets of one of these types. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "sources", "in": "query", "description": "Only subnets that came to exist one of these ways (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "managed_by", "in": "query", "description": "Only bridge subnets created by one of these container runtimes. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "user_managed", "in": "query", "description": "`true` returns only the subnets the user curates, leaving out the ones Scanopy fabricates\n(`Subnet::is_user_managed`).", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns subnets that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the subnet must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SubnetOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/SubnetOrderField" }, { "type": "null" } ] } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "stale", "in": "query", "description": "`true` returns only subnets discovery hasn't observed within their\nsite's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the subnet's own site's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the subnet's\nname, CIDR and description.", "required": false, "schema": { "type": [ "string", "null" ] } } ], "responses": { "200": { "description": "Distinct values and their Subnet counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/subnets/{id}": { "get": { "tags": [ "Subnets" ], "summary": "Get subnet by ID", "description": "Returns the subnet with its utilization and the range it sits inside, if any.", "operationId": "get_subnet_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Subnet ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Subnet found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_SubnetResponse" } } } }, "404": { "description": "Subnet not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Subnets" ], "summary": "Update a subnet", "description": "Updates subnet properties. If the CIDR is being changed, validates that\nall existing ip_addresses on this subnet have IPs within the new CIDR range.", "operationId": "update_subnet", "parameters": [ { "name": "id", "in": "path", "description": "Subnet ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Subnet" } } }, "required": true }, "responses": { "200": { "description": "Subnet updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Subnet" } } } }, "400": { "description": "CIDR change would orphan existing ip_addresses", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Subnet not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Subnets" ], "summary": "Delete Subnet", "operationId": "delete_subnet", "parameters": [ { "name": "id", "in": "path", "description": "Subnet ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Subnet deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Subnet not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/subnets/{id}/merge": { "post": { "tags": [ "Subnets" ], "summary": "Merge a subnet into the range that contains it", "description": "Moves every address to the covering subnet and removes this one. Offered for a range Scanopy\nassumed that a later reading turned out to cover: discovery corrects such a range on its own\nonly where the answer is unambiguous, and folding several assumed ranges into one means deleting\nrows, which is a person's call rather than a scan's.", "operationId": "merge_subnet", "parameters": [ { "name": "id", "in": "path", "description": "Subnet ID to merge away", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MergeSubnetRequest" } } }, "required": true }, "responses": { "200": { "description": "Subnet merged", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Subnet" } } } }, "400": { "description": "The target does not contain this subnet", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Subnet not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tag-groups": { "get": { "tags": [ "Tag Groups" ], "summary": "List all tag groups", "description": "Returns every tag group in your organization: the system groups (Application, Status) first,\nthen the groups your organization named, by name.", "operationId": "get_all_tag_groups", "parameters": [ { "name": "system", "in": "query", "description": "Only system groups (`true`), only the organization's own (`false`), or both (repeat it).\nSent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "ordered", "in": "query", "description": "Only groups keeping their tags in a set order (`true`), by name (`false`), or both (repeat\nit). Sent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "entity_types", "in": "query", "description": "Only groups that apply to one of these entity types. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntityDiscriminants" } } }, { "name": "include_every_entity_type", "in": "query", "description": "`true` also returns groups that apply to every entity type. Set on its own it returns only\nthose.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "tag_ids", "in": "query", "description": "Only groups holding one of these tags. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and description.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TagGroupOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TagGroupOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of tag groups", "content": { "application/json": { "schema": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/TagGroupBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "A named set of tags of which an entity holds at most one, in an order the organization sets,\noptionally limited to some entity types.", "example": { "created_at": "2026-01-15T10:30:00Z", "description": "Which stage of delivery an asset serves.", "entity_types": null, "group_type": "Custom", "id": "550e8400-e29b-41d4-a716-446655440010", "name": "Environment", "ordered": true, "organization_id": "550e8400-e29b-41d4-a716-446655440001", "scope_computed": false, "updated_at": "2026-01-15T10:30:00Z" } }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Tag Groups" ], "summary": "Create a tag group", "description": "Creates a group your organization names. Group names are unique within the organization, and\n\"Application\" and \"Status\" are reserved for the system groups.", "operationId": "create_tag_group", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TagGroup" } } }, "required": true }, "responses": { "200": { "description": "Tag group created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_TagGroup" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tag-groups/bulk-delete": { "post": { "tags": [ "Tag Groups" ], "summary": "Bulk delete Tag Groups", "operationId": "bulk_delete_tag_groups", "requestBody": { "description": "Array of Tag Group IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Tag Groups deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tag-groups/{id}": { "get": { "tags": [ "Tag Groups" ], "summary": "Get Tag Group by ID", "operationId": "get_tag_group_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Tag Group ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Tag Group found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_TagGroup" } } } }, "404": { "description": "Tag Group not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Tag Groups" ], "summary": "Update Tag Group", "operationId": "update_tag_group", "parameters": [ { "name": "id", "in": "path", "description": "Tag Group ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TagGroup" } } }, "required": true }, "responses": { "200": { "description": "Tag Group updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_TagGroup" } } } }, "404": { "description": "Tag Group not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Tag Groups" ], "summary": "Delete Tag Group", "operationId": "delete_tag_group", "parameters": [ { "name": "id", "in": "path", "description": "Tag Group ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Tag Group deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Tag Group not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tag-groups/{id}/tag-order": { "put": { "tags": [ "Tag Groups" ], "summary": "Set the order of a tag group's tags", "description": "Tables, pickers and the topology list a group's tags in this order.", "operationId": "set_tag_order", "parameters": [ { "name": "id", "in": "path", "description": "Tag group ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TagOrderRequest" } } }, "required": true }, "responses": { "200": { "description": "The group's tags, in their new order", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_Tag" } } } }, "400": { "description": "The list doesn't name every tag in the group exactly once", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Tag group not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tags": { "get": { "tags": [ "Tags" ], "summary": "List all tags", "description": "Returns all tags in the authenticated user's organization.\nSupports pagination via `limit` and `offset` query parameters,\nand ordering via `group_by`, `order_by`, and `order_direction`.", "operationId": "get_all_tags", "parameters": [ { "name": "colors", "in": "query", "description": "Only tags of one of these colours. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "tag_group_ids", "in": "query", "description": "Only tags in one of these groups. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "icons", "in": "query", "description": "Only tags with one of these icons. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and description.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TagOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TagOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "List of tags", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Tag" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Tags" ], "summary": "Create a new tag", "description": "Creates a tag scoped to your organization. Tag names must be unique within the organization.\n\n### Validation\n\n- Name must be 1-100 characters (empty names are rejected)\n- Name must be unique within your organization", "operationId": "create_tag", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Tag" } } }, "required": true }, "responses": { "200": { "description": "Tag created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Tag" } } } }, "400": { "description": "Validation error: name empty or too long", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Tag name already exists in this organization", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tags/assign": { "put": { "tags": [ "Tags" ], "summary": "Set all tags for an entity", "description": "Replaces all tags on an entity with the provided list.\n\n### Validation\n\n- Entity type must be taggable (Host, Service, Subnet, Vlan, Dependency, Site, Discovery, Daemon, DaemonApiKey, UserApiKey, Credential)\n- All tags must exist and belong to your organization", "operationId": "set_entity_tags", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetTagsRequest" } } }, "required": true }, "responses": { "200": { "description": "Tags set successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Invalid entity type or tag", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Tag not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tags/assign/bulk-add": { "post": { "tags": [ "Tags" ], "summary": "Bulk add a tag to multiple entities", "description": "Adds a single tag to multiple entities of the same type. This is useful for batch tagging operations.\n\n### Validation\n\n- Entity type must be taggable (Host, Service, Subnet, Vlan, Dependency, Site, Discovery, Daemon, DaemonApiKey, UserApiKey, Credential)\n- Tag must exist and belong to your organization\n- Entities that already have the tag are silently skipped", "operationId": "bulk_add_tag", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BulkTagRequest" } } }, "required": true }, "responses": { "200": { "description": "Tag added successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkTagResponse" } } } }, "400": { "description": "Invalid entity type or tag", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Tag not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tags/assign/bulk-remove": { "post": { "tags": [ "Tags" ], "summary": "Bulk remove a tag from multiple entities", "description": "Removes a single tag from multiple entities of the same type.\n\n### Validation\n\n- Entity type must be taggable (Host, Service, Subnet, Vlan, Dependency, Site, Discovery, Daemon, DaemonApiKey, UserApiKey, Credential)\n- Entities that don't have the tag are silently skipped", "operationId": "bulk_remove_tag", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BulkTagRequest" } } }, "required": true }, "responses": { "200": { "description": "Tag removed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkTagResponse" } } } }, "400": { "description": "Invalid entity type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tags/bulk-delete": { "post": { "tags": [ "Tags" ], "summary": "Bulk delete Tags", "operationId": "bulk_delete_tags", "requestBody": { "description": "Array of Tag IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Tags deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tags/export/csv": { "get": { "tags": [ "Tags" ], "summary": "Export Tags to CSV", "description": "Export all Tags matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_tags_csv", "parameters": [ { "name": "colors", "in": "query", "description": "Only tags of one of these colours. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "tag_group_ids", "in": "query", "description": "Only tags in one of these groups. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "icons", "in": "query", "description": "Only tags with one of these icons. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and description.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TagOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TagOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "CSV file containing Tags", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tags/field-values/{field}": { "get": { "tags": [ "Tags" ], "summary": "List the values of a Tag field", "description": "Every distinct value of one Tag field across the Tags the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_tag_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/TagOrderField" } }, { "name": "colors", "in": "query", "description": "Only tags of one of these colours. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "tag_group_ids", "in": "query", "description": "Only tags in one of these groups. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "icons", "in": "query", "description": "Only tags with one of these icons. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the name and description.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TagOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TagOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } } ], "responses": { "200": { "description": "Distinct values and their Tag counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/tags/{id}": { "get": { "tags": [ "Tags" ], "summary": "Get Tag by ID", "operationId": "get_tag_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Tag ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Tag found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Tag" } } } }, "404": { "description": "Tag not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Tags" ], "summary": "Update Tag", "operationId": "update_tag", "parameters": [ { "name": "id", "in": "path", "description": "Tag ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Tag" } } }, "required": true }, "responses": { "200": { "description": "Tag updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Tag" } } } }, "404": { "description": "Tag not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Tags" ], "summary": "Delete Tag", "operationId": "delete_tag", "parameters": [ { "name": "id", "in": "path", "description": "Tag ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Tag deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Tag not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/topology": { "get": { "tags": [ "Topologies" ], "summary": "Get all topologies for the authenticated user's sites.", "description": "Returns both live-view rows (`snapshot_id IS NULL`) and snapshot-pinned\nrows. The frontend renders the live one by default and renders snapshot\nrows when the user picks one from the snapshots dropdown.", "operationId": "get_all_topologies", "parameters": [ { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of topologies", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Topology" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/topology/data": { "get": { "tags": [ "Topologies", "internal" ], "summary": "Unified entity-set endpoint for the topology view.", "description": "`?snapshot_id=` resolves to the snapshot's `taken_at` and returns the\nas-of-T entity set; otherwise returns live entities. The frontend\n`TopologyTab` is the sole intended consumer.", "operationId": "get_topology_data", "parameters": [ { "name": "site_id", "in": "query", "description": "Site to read entities for. Required.", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "snapshot_id", "in": "query", "description": "When set, returns the entity set as it was when this snapshot was taken.\nWhen omitted, returns live entities.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "mark_viewed", "in": "query", "description": "When `true`, records the `FirstTopologyRebuild` onboarding milestone (the user has\nviewed their topology). Only the frontend's explicit on-tab view sets this — the\nbackground topology-data query never does — so the milestone never fires from other\ntabs. One-time per org (guarded below + subscriber dedup).", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "view", "in": "query", "description": "The view to build the graph for. When omitted, the graph of every view is built and\nreturned. The topology tab always sends the view it draws.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/TopologyView" }, { "type": "null" } ] } } ], "responses": { "200": { "description": "Topology entity bundle", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_TopologyData" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Snapshot not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/topology/export/csv": { "get": { "tags": [ "Topologies" ], "summary": "Export Topologies to CSV", "description": "Export all Topologies matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_topologies_csv", "parameters": [ { "name": "site_id", "in": "query", "description": "Filter by site ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "ids", "in": "query", "description": "Filter by specific entity IDs (for selective loading)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing Topologies", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/topology/{id}": { "get": { "tags": [ "Topologies" ], "summary": "Get Topology by ID", "operationId": "get_topology_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Topology ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Topology found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Topology" } } } }, "404": { "description": "Topology not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Topologies" ], "operationId": "update_topology", "parameters": [ { "name": "id", "in": "path", "description": "Topology ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Topology" } } }, "required": true }, "responses": { "200": { "description": "Topology updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Topology" } } } }, "404": { "description": "Topology not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/topology/{id}/export/confluence": { "get": { "tags": [ "Topologies" ], "summary": "Export topology as Confluence wiki markup", "operationId": "export_confluence", "parameters": [ { "name": "id", "in": "path", "description": "Topology ID", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "view", "in": "query", "description": "View to export. Defaults to the default view when omitted.", "required": false, "schema": { "$ref": "#/components/schemas/TopologyView" } } ], "responses": { "200": { "description": "Confluence wiki markup export", "content": { "text/plain": { "schema": { "type": "string" } } } }, "402": { "description": "Confluence export not available on plan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Topology not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/topology/{id}/export/mermaid": { "get": { "tags": [ "Topologies" ], "summary": "Export topology as Mermaid flowchart", "operationId": "export_mermaid", "parameters": [ { "name": "id", "in": "path", "description": "Topology ID", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "view", "in": "query", "description": "View to export. Defaults to the default view when omitted.", "required": false, "schema": { "$ref": "#/components/schemas/TopologyView" } } ], "responses": { "200": { "description": "Mermaid flowchart export", "content": { "text/plain": { "schema": { "type": "string" } } } }, "402": { "description": "Mermaid export not available on plan", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Topology not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/users": { "get": { "tags": [ "Users" ], "summary": "List all users", "operationId": "get_all_users", "parameters": [ { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "List of users", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_User" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/users/bulk-delete": { "post": { "tags": [ "Users" ], "summary": "Bulk delete users", "operationId": "bulk_delete_users", "requestBody": { "description": "Array of User IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Users deleted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } }, "403": { "description": "Cannot delete yourself, or a user whose role you could not grant", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/users/export/csv": { "get": { "tags": [ "Users" ], "summary": "Export Users to CSV", "description": "Export all Users matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_users_csv", "parameters": [ { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "CSV file containing Users", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/users/members": { "get": { "tags": [ "Users" ], "summary": "List members", "description": "The organization's users and its pending invites in one list, each limited to the ones the\ncaller may see: every user and invite for an owner, otherwise the caller themselves, members\nbelow their role, and invites they created.", "operationId": "list_members", "parameters": [ { "name": "statuses", "in": "query", "description": "Only members in one of these statuses. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/MemberStatus" } } }, { "name": "permissions", "in": "query", "description": "Only members with one of these roles. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/UserOrgPermissions" } } }, { "name": "site_ids", "in": "query", "description": "Only users assigned one of these sites. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "all_sites", "in": "query", "description": "`true` also returns admins and owners, who reach every site. Set on its own it returns only\nthose.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "auth_methods", "in": "query", "description": "Only users signing in one of these ways (``, `+password`, `password`,\n`none`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "invited_by", "in": "query", "description": "Only invites created by one of these users. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "email_verified", "in": "query", "description": "Only users whose email is verified (`true`), unverified (`false`), or both (repeat it).\nSent empty, matches none.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "boolean" } } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the email.", "required": false, "schema": { "type": [ "string", "null" ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/MemberOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/MemberOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } } ], "responses": { "200": { "description": "Users and pending invites", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Member" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/users/{id}": { "get": { "tags": [ "Users" ], "summary": "Get user by ID", "operationId": "get_user_by_id", "parameters": [ { "name": "id", "in": "path", "description": "User ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "User found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "403": { "description": "Access denied", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "User not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] }, "put": { "tags": [ "Users" ], "summary": "Update your own user record", "operationId": "update_user", "parameters": [ { "name": "id", "in": "path", "description": "User ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } }, "required": true }, "responses": { "200": { "description": "User updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "403": { "description": "Cannot update another user's record", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "User not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "session": [] } ] }, "delete": { "tags": [ "Users" ], "summary": "Delete a user", "operationId": "delete_user", "parameters": [ { "name": "id", "in": "path", "description": "User ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "User deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "403": { "description": "Cannot delete yourself, or a user whose role you could not grant", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "User not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "Cannot delete the only owner", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/users/{id}/admin": { "put": { "tags": [ "Users", "internal" ], "summary": "Admin update user (for changing permissions)", "operationId": "admin_update_user", "parameters": [ { "name": "id", "in": "path", "description": "User ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } }, "required": true }, "responses": { "200": { "description": "User updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_User" } } } }, "403": { "description": "Cannot update user with higher permissions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "User not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/vlans": { "get": { "tags": [ "Vlans" ], "summary": "List all VLANs", "description": "Returns VLANs accessible to the authenticated user, optionally filtered by site.", "operationId": "get_all_vlans", "parameters": [ { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/VlanOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/VlanOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "sources", "in": "query", "description": "Filter by how the VLAN came to exist (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "subnet_ids", "in": "query", "description": "Filter to VLANs linked to one of these subnets. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns VLANs that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the VLAN must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the VLAN ID,\nname and description.", "required": false, "schema": { "type": [ "string", "null" ] } } ], "responses": { "200": { "description": "List of VLANs", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Vlan" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "post": { "tags": [ "Vlans" ], "summary": "Create a new VLAN", "description": "Creates a VLAN scoped to a site. VLAN numbers must be unique within a site.", "operationId": "create_vlan", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Vlan" } } }, "required": true }, "responses": { "200": { "description": "VLAN created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vlan" } } } }, "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "409": { "description": "VLAN number already exists in this site", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/vlans/bulk-delete": { "post": { "tags": [ "Vlans" ], "summary": "Bulk delete Vlans", "operationId": "bulk_delete_vlans", "requestBody": { "description": "Array of Vlan IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Vlans deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_BulkDeleteResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/vlans/discovery": { "post": { "tags": [ "Vlans" ], "summary": "Bulk upsert VLANs from discovery", "description": "Used by daemons to report discovered VLANs. Creates new VLANs or updates names.\nReturns the mapping of VLAN numbers to entity UUIDs for Interface construction.", "operationId": "discovery_upsert_vlans", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VlanDiscoveryRequest" } } }, "required": true }, "responses": { "200": { "description": "VLANs upserted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_VlanDiscoveryResponse" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "daemon_api_key": [] } ] } }, "/api/v1/vlans/export/csv": { "get": { "tags": [ "Vlans" ], "summary": "Export Vlans to CSV", "description": "Export all Vlans matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_vlans_csv", "parameters": [ { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/VlanOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/VlanOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "sources", "in": "query", "description": "Filter by how the VLAN came to exist (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "subnet_ids", "in": "query", "description": "Filter to VLANs linked to one of these subnets. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns VLANs that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the VLAN must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the VLAN ID,\nname and description.", "required": false, "schema": { "type": [ "string", "null" ] } } ], "responses": { "200": { "description": "CSV file containing Vlans", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/vlans/field-values/{field}": { "get": { "tags": [ "Vlans" ], "summary": "List the values of a Vlan field", "description": "Every distinct value of one Vlan field across the Vlans the caller can list, with how many hold each. Takes the list's filters; ignores pagination, search and ordering.", "operationId": "get_vlan_field_values", "parameters": [ { "name": "field", "in": "path", "description": "The field whose values to count", "required": true, "schema": { "$ref": "#/components/schemas/VlanOrderField" } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/VlanOrderField" }, { "type": "null" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/VlanOrderField" }, { "type": "null" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "$ref": "#/components/schemas/OrderDirection" }, { "type": "null" } ] } }, { "name": "limit", "in": "query", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "maximum": 1000, "minimum": 0 } }, { "name": "offset", "in": "query", "description": "Number of results to skip. Default: 0.", "required": false, "schema": { "type": [ "integer", "null" ], "format": "int32", "minimum": 0 } }, { "name": "site_ids", "in": "query", "description": "Filter by site ID. Repeat the parameter to pass several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "sources", "in": "query", "description": "Filter by how the VLAN came to exist (`source.type`). Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntitySourceDiscriminants" } } }, { "name": "subnet_ids", "in": "query", "description": "Filter to VLANs linked to one of these subnets. Repeat for several.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_ids", "in": "query", "description": "Filter by tag IDs (returns VLANs that have ANY of the specified tags)", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" } } }, { "name": "tag_sets", "in": "query", "description": "Tag sets the VLAN must hold one tag from each of. Each entry is a comma-separated list\nof tag ids. One entry per tag group the list is filtered by.", "required": false, "schema": { "type": [ "array", "null" ], "items": { "type": "string" } } }, { "name": "group_by_tag_group", "in": "query", "description": "Group by the tag each row holds from this tag group. Takes the place of `group_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "order_by_tag_group", "in": "query", "description": "Sort by the position of the tag each row holds from this tag group. Takes the place of\n`order_by`.", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "at", "in": "query", "description": "As-of timestamp (ISO 8601). When set, returns SCD2 state as of this\ninstant (snapshot view) instead of live state.", "required": false, "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the VLAN ID,\nname and description.", "required": false, "schema": { "type": [ "string", "null" ] } } ], "responses": { "200": { "description": "Distinct values and their Vlan counts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vec_GroupCount" } } } }, "400": { "description": "Unknown field", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/vlans/{id}": { "get": { "tags": [ "Vlans" ], "summary": "Get Vlan by ID", "operationId": "get_vlan_by_id", "parameters": [ { "name": "id", "in": "path", "description": "Vlan ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Vlan found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vlan" } } } }, "404": { "description": "Vlan not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Vlans" ], "summary": "Update Vlan", "operationId": "update_vlan", "parameters": [ { "name": "id", "in": "path", "description": "Vlan ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Vlan" } } }, "required": true }, "responses": { "200": { "description": "Vlan updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Vlan" } } } }, "404": { "description": "Vlan not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Vlans" ], "summary": "Delete Vlan", "operationId": "delete_vlan", "parameters": [ { "name": "id", "in": "path", "description": "Vlan ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Vlan deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "404": { "description": "Vlan not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/version": { "get": { "tags": [ "system", "internal" ], "summary": "Get API version information", "operationId": "get_version", "responses": { "200": { "description": "Version information", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_VersionInfo" } } } } } } } }, "components": { "schemas": { "ApiErrorResponse": { "type": "object", "description": "Error response type for API errors (no data field)", "required": [ "success", "meta" ], "properties": { "code": { "type": [ "string", "null" ], "description": "Machine-readable error code for i18n translation" }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API metadata (version info)" }, "params": { "type": [ "object", "null" ], "description": "Parameters for interpolating into the translated error message", "additionalProperties": {}, "propertyNames": { "type": "string" } }, "success": { "type": "boolean", "description": "Always `false` on this response shape." } } }, "ApiMeta": { "type": "object", "description": "API metadata included in all responses", "required": [ "api_version", "server_version" ], "properties": { "api_version": { "type": "integer", "format": "int32", "description": "API version (integer, increments on breaking changes)", "minimum": 0 }, "server_version": { "type": "string", "description": "Server version (semver)", "example": "0.19.0" } }, "example": { "api_version": 1, "server_version": "0.19.0" } }, "ApiResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "oneOf": [ { "$ref": "#/components/schemas/TupleUnit", "description": "The result payload." }, { "type": "null" } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Binding": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/BindingBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "Association between a service and a port / interface that the service is listening on", "example": { "created_at": "2026-10-22T23:24:30.832627Z", "first_discovery_id": null, "id": "fc6c2dca-1633-473a-bfca-7132fb921d9b", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-10-22T23:24:30.832627Z", "lineage_id": null, "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Port", "updated_at": "2026-10-22T23:24:30.832627Z", "valid_from": "2026-10-22T23:24:30.832627Z", "valid_to": null } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_BulkDeleteResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The result payload. Omitted on failure.", "required": [ "deleted_count", "requested_count" ], "properties": { "deleted_count": { "type": "integer", "description": "How many records were actually deleted.", "minimum": 0 }, "requested_count": { "type": "integer", "description": "How many IDs the request asked to delete.", "minimum": 0 } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_BulkTagResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response for bulk tag operations", "required": [ "affected_count" ], "properties": { "affected_count": { "type": "integer", "description": "Number of entities affected", "minimum": 0 } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_CancelSubscriptionResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The result payload. Omitted on failure.", "required": [ "period_end" ], "properties": { "period_end": { "type": "string", "format": "date-time", "description": "When the current paid period ends and access drops to the free tier." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_ChangePlanPreview": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The result payload. Omitted on failure.", "required": [ "excess_hosts", "excess_sites", "excess_seats" ], "properties": { "excess_hosts": { "type": "integer", "format": "int64", "description": "Hosts over the target plan's allowance, which would be billed as overage.", "minimum": 0 }, "excess_seats": { "type": "integer", "format": "int64", "description": "Seats over the target plan's allowance.", "minimum": 0 }, "excess_sites": { "type": "integer", "format": "int64", "description": "Sites over the target plan's allowance.", "minimum": 0 } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_CheckEmailResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Check email availability response.\n\nAn address that is already registered is an answer to this question, not a\nfailure of it: the caller asked whether the address is free and got a\ntruthful no. Returning an error for that case forced every client to either\ntreat a 409 as success or suppress the toast that came with it.", "required": [ "available" ], "properties": { "available": { "type": "boolean", "description": "Whether the address has no account yet, and so can be registered." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Credential": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/CredentialBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "is_default" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "is_default": { "type": "boolean", "description": "Whether this is the organization's seeded default for its type: identical to the\ncredential every organization starts with. A default can't be deleted, and its settings\ncan't change. Computed from `credential_type`, so a value sent in a request is ignored.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Daemon": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/DaemonBase" }, { "type": "object", "required": [ "id", "updated_at", "created_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_DaemonApiKey": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/DaemonApiKeyBase" }, { "type": "object", "required": [ "id", "updated_at", "created_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_DaemonApiKeyResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The result payload. Omitted on failure.", "required": [ "api_key", "key" ], "properties": { "api_key": { "$ref": "#/components/schemas/DaemonApiKey", "description": "The stored key record." }, "key": { "type": "string", "format": "password", "description": "The plaintext API key - only returned once during creation or rotation.", "readOnly": true } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_DaemonRegistrationResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Daemon registration response from server to daemon", "required": [ "daemon", "host_id" ], "properties": { "daemon": { "$ref": "#/components/schemas/Daemon", "description": "The registered daemon record." }, "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "server_capabilities": { "oneOf": [ { "$ref": "#/components/schemas/ServerCapabilities", "description": "Server capabilities (returned if daemon sends version info)" }, { "type": "null" } ] } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_DaemonResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/DaemonBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "version_status", "interfaced_subnet_ids", "health" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "health": { "$ref": "#/components/schemas/DaemonHealth", "description": "The one status the daemon's chip shows, the most severe that applies." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "interfaced_subnet_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Subnets this daemon has interfaces on, loaded from the\n`daemon_interfaced_subnets` junction (replaces the old\n`capabilities.interfaced_subnet_ids` JSONB field)." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "version_status": { "$ref": "#/components/schemas/DaemonVersionStatus", "description": "Computed version status including health and warnings" } } } ], "description": "Daemon response for UI including computed version status" }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_DashboardSummary": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Dashboard summary response", "required": [ "sites", "daemons", "recent_discoveries", "plan_usage", "uncovered_integrations" ], "properties": { "daemons": { "type": "array", "items": { "$ref": "#/components/schemas/DaemonResponse" }, "description": "Daemons the caller can see, with their current status." }, "plan_usage": { "$ref": "#/components/schemas/PlanUsage", "description": "Current usage against the organization's plan allowances." }, "recent_discoveries": { "type": "array", "items": { "$ref": "#/components/schemas/RecentDiscovery" }, "description": "The most recent discovery runs, newest first." }, "sites": { "type": "array", "items": { "$ref": "#/components/schemas/SiteSummary" }, "description": "Per-site counts for every site the caller can see." }, "uncovered_integrations": { "type": "array", "items": { "$ref": "#/components/schemas/UncoveredIntegration" }, "description": "Integrations found on hosts with no credential of them, most hosts first." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_DemoPopulateStatus": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "oneOf": [ { "type": "object", "title": "Running", "required": [ "started_at", "state" ], "properties": { "started_at": { "type": "string", "format": "date-time", "description": "When population began." }, "state": { "type": "string", "enum": [ "running" ] } } }, { "type": "object", "title": "Complete", "required": [ "finished_at", "state" ], "properties": { "finished_at": { "type": "string", "format": "date-time", "description": "When population finished." }, "state": { "type": "string", "enum": [ "complete" ] } } }, { "type": "object", "title": "Failed", "required": [ "error", "finished_at", "state" ], "properties": { "error": { "type": "string", "description": "Why population failed." }, "finished_at": { "type": "string", "format": "date-time", "description": "When it gave up." }, "state": { "type": "string", "enum": [ "failed" ] } } } ], "description": "Lifecycle of a demo-populate task. `Running` is set synchronously in the\nPOST handler (before the `202`), then flipped to a terminal variant by the\nspawned task. `Failed` carries the error string so the UI can show why." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Dependency": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/DependencyBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure.", "example": { "color": "Blue", "created_at": "2026-01-15T10:30:00Z", "dependency_type": "RequestPath", "description": "HTTP/HTTPS services dependency", "edge_style": "Bezier", "id": "550e8400-e29b-41d4-a716-446655440008", "lineage_id": null, "members": { "service_ids": [], "type": "Services" }, "name": "Web Services", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Discovery": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/DiscoveryBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "integration_targets" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "force_full_scan": { "type": "boolean", "description": "When true, the next scan will be a full port scan regardless of interval" }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "integration_targets": { "type": "array", "items": { "$ref": "#/components/schemas/IntegrationTarget" }, "description": "Per-daemon integration targeting: which integrations run on this daemon, and on which\nIPs. Delivered via the init command at registration and editable via the discovery\nmodal. This is the single home for cred↔IP targeting; it replaces the global\n`credential.target_ips` (race-prone, consumed once).\n\nOne-shot: a target is offered to the daemon until a scan completes successfully, then\ndropped by [`Discovery::apply_successful_scan`]. Credentials that earned a durable home\nduring the scan keep being retried from there — `host_credentials` for one that probed\nsuccessfully, `site_credentials` for a broadcast one (see\n[`Discovery::take_site_scope_credential_ids`])." }, "next_run_is_full_scan": { "type": "boolean", "description": "Whether the next run of this discovery will be a full scan of all 65,535 ports rather than\na light scan of common ports. Computed by the server on read and never stored.", "readOnly": true }, "scan_count": { "type": "integer", "format": "int32", "description": "Number of completed scans (incremented by server on session completion)", "readOnly": true, "minimum": 0 }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_DiscoveryUpdatePayload": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Progress update from daemon to server during discovery", "required": [ "session_id", "daemon_id", "site_id", "phase", "discovery_type", "progress" ], "properties": { "credential_results": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialRunResult" }, "description": "What each stored credential did in this run, one entry per credential. Results, not\nproblems: those are `warnings`. Old servers ignore it.\n\nRead per entry, like `warnings`: an outcome shape from a newer daemon becomes `Unknown`\nfor that credential instead of failing the terminal payload." }, "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon this entity refers to." }, "daemon_version": { "type": [ "string", "null" ], "description": "The daemon's version when the run ended. Stamped by the server at terminal, so it\nsurvives the daemon being deleted or upgraded." }, "discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery configuration this session belongs to.\nAlways enriched server-side; daemons do not send this field." }, "discovery_type": { "$ref": "#/components/schemas/DiscoveryType", "description": "What type of discovery is running." }, "error": { "type": [ "string", "null" ], "description": "Failure message, when the run did not complete." }, "estimated_remaining_secs": { "type": [ "integer", "null" ], "format": "int32", "description": "Rough estimate of the time left, in seconds.", "minimum": 0 }, "finished_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the run finished. `null` while it is still going." }, "hosts_discovered": { "type": [ "integer", "null" ], "format": "int32", "description": "Hosts found so far.", "minimum": 0 }, "last_update_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the server last heard about this run. Stamped by the server at terminal." }, "phase": { "$ref": "#/components/schemas/DiscoveryPhase", "description": "Which stage of the run is in progress." }, "progress": { "type": "integer", "format": "int32", "description": "Completion of the current phase, from 0 to 1.", "minimum": 0 }, "reason": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryTerminalReason", "description": "Why the run ended. Set on terminal payloads; absent on runs recorded before it existed.\nA daemon may send one of the reasons only it can know; the server assigns every other.\nA reason this server doesn't know reads as `None`, and the server assigns one from the\nphase." }, { "type": "null" } ] }, "scanned": { "oneOf": [ { "$ref": "#/components/schemas/ScannedEntityIds", "description": "Canonical IDs of entities scanned in this session, populated daemon-\nside at terminal. **Transient**: stripped at `SqlValue::RunType` bind\ntime so it doesn't persist into the historical Discovery row's JSONB.\nAvailable in-memory through the `EntityOperation::Created` event\npublished for the historical Discovery row (the event scope carries\n`Entity::Discovery(...)`, the full in-memory struct), where per-entity\nFK-update subscribers consume it.\n\n`Some(...)` when the daemon is sending the terminal payload over the\nwire. `None` when read back from a persisted historical row, or when\nnot yet set." }, { "type": "null" } ] }, "session_id": { "type": "string", "format": "uuid", "description": "The discovery run this update belongs to." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "started_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the run started." }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/DiscoveryWarning" }, "description": "Non-fatal findings from a completed run — one per occurrence, each carrying the code that\nidentifies it and the detail that fills the sentence. Unlike `error`, these do not mark the\nrun failed.\n\nRead through [`deserialize_warnings`] rather than the derived impl, which is what keeps\nhistorical records and pre-coded daemons rendering: both send bare strings here, and both\nland as `Unknown` carrying that text instead of failing the whole payload." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_EntitlementResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Data returned inside `ApiResponse` by [`ENTITLEMENT_PATH`].", "required": [ "entitlement" ], "properties": { "entitlement": { "type": "string", "description": "An offline-format license key (claims: [`super::types::LicenseClaims`])\nwith `org_id` and `plan` set from the organization's current state." }, "instances": { "oneOf": [ { "$ref": "#/components/schemas/InstanceUsage", "description": "How many server instances the key is running on, against how many it\ncovers. Absent from clouds older than instance counting." }, { "type": "null" } ] } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_GlobalSearchResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Matches grouped by entity type, in registry order. Types with no matches are left out.", "required": [ "groups" ], "properties": { "groups": { "type": "array", "items": { "$ref": "#/components/schemas/GlobalSearchGroup" }, "description": "One group per entity type with at least one match." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_HostResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response type for host endpoints.\nIncludes children (ip_addresses, ports, services, interfaces).", "required": [ "id", "created_at", "updated_at", "last_seen_at", "name", "site_id", "source", "hidden", "tags", "firmware_revision", "software_revision", "os", "ip_addresses", "ports", "services", "interfaces" ], "properties": { "asset_tag": { "type": [ "string", "null" ], "description": "The asset tag the device reports: ENTITY-MIB entPhysicalAssetID. Read-only: discovery is\nthe only writer, so it always says what the device itself carries.", "readOnly": true }, "asset_tag_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the asset tag. Read-only.", "readOnly": true }, "chassis_id": { "type": [ "string", "null" ], "description": "LLDP chassis identifier, used to match the host to its neighbours." }, "chassis_id_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the LLDP chassis identifier. Read-only: decided by whichever source read it.", "readOnly": true }, "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "credential_assignments": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialAssignment" }, "description": "Credentials assigned to scan this host." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the host." }, "device_type": { "$ref": "#/components/schemas/DeviceType", "description": "What this host is, as a device: the role its icon shows everywhere it appears. Computed\nby [`Host::device_type`] from its services, then its OS, as the topology computes it.\nRead-only.", "readOnly": true }, "display_name": { "type": [ "string", "null" ], "description": "What to call this host when `name` is empty: its hostname, sysName, chassis id or first\naddress, whichever it has. `None` when nothing identifies it.\n\nRead-only and separate from `name` rather than folded into it. `name` is what a person\ntyped and what the editor writes back, so filling it with a fallback would turn a chassis\nid into a name the next save persists. Computed from [`Host::display_name`], the same\nladder topology titles a host container with, so a host cannot be called one thing in the\nlist and another on the map.", "readOnly": true }, "display_name_rung": { "oneOf": [ { "$ref": "#/components/schemas/HostNameRung", "description": "Which rung of the ladder produced `display_name`. `None` exactly when `display_name` is.\n\nResolved from the same [`Host::name_ladder`] call as `display_name`, so the UI can say\nwhere a host's title came from without walking the rungs itself." }, { "type": "null" } ], "readOnly": true }, "firmware_revision": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalFirmwareRev — firmware revision of the device. Read-only, as above.", "readOnly": true }, "firmware_revision_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the firmware revision. Read-only: decided by whichever source read it.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery run that first observed this host. Drives the \"First found by\" column.", "readOnly": true }, "hidden": { "type": "boolean", "description": "Always `false`. Kept because daemons before v0.17.23 require it when they read a host\nback; a host is hidden from topology by a tag of the Visibility group.", "readOnly": true }, "hostname": { "type": [ "string", "null" ], "description": "Hostname as resolved or reported by the host." }, "hostname_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced `hostname`: a PTR lookup, the host's own OS, a controller, mDNS, or a person.\nRead-only: decided by whichever source read it.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/Interface" }, "description": "SNMP ifTable entries" }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "IP addresses on this host." }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery run that last observed this host. Drives the \"Last found by\" column.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "Last time discovery observed this host. User-facing (drives the \"Last\nseen\" column and the stale badge), which is why it is carried here while\nthe version-history columns are not." }, "management_url": { "type": [ "string", "null" ], "description": "Link to the host's own management interface." }, "management_url_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the management URL. Read-only: decided by whichever source read it.", "readOnly": true }, "manufacturer": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalMfgName — hardware manufacturer. Read-only, as above.", "readOnly": true }, "manufacturer_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the manufacturer. Read-only: decided by whichever source read it.", "readOnly": true }, "model": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalModelName — hardware model. Read-only, as above.", "readOnly": true }, "model_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the model. Read-only: decided by whichever source read it.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for the host." }, "name_ladder": { "type": "array", "items": { "$ref": "#/components/schemas/HostNameLadderEntry", "readOnly": true }, "description": "Every rung of the display-name ladder for this host, highest first, with what each holds." }, "name_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced `name`. Read-only: it is decided by whoever supplied the name, not by the\ncaller.", "readOnly": true }, "os": { "oneOf": [ { "$ref": "#/components/schemas/HostOs", "description": "The host's operating system. Read-only: written by whichever source read or matched it.\nLenient, so a daemon tolerates a family added on a newer server." }, { "type": "null" } ], "readOnly": true }, "os_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the OS. Read-only, as above. A source whose method is `Inferred` matched the OS\nin a string the host emitted rather than reading it off the host.", "readOnly": true }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/Port" }, "description": "Open ports on this host." }, "serial_number": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalSerialNum — hardware serial number. Read-only, as above.", "readOnly": true }, "serial_number_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the serial number. Read-only: decided by whichever source read it.", "readOnly": true }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Services running on this host." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "software_revision": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalSoftwareRev — software revision of the device. Read-only, as above.", "readOnly": true }, "software_revision_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the software revision. Read-only: decided by whichever source read it.", "readOnly": true }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "How this host came to be known — discovered, imported, or created by hand." }, "sys_contact": { "type": [ "string", "null" ], "description": "SNMP sysContact — administrative contact as configured on the device." }, "sys_contact_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysContact. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_descr": { "type": [ "string", "null" ], "description": "SNMP sysDescr — the device's own description of itself." }, "sys_descr_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysDescr. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_location": { "type": [ "string", "null" ], "description": "SNMP sysLocation — physical location as configured on the device." }, "sys_location_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysLocation. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_name": { "type": [ "string", "null" ], "description": "SNMP sysName.0 — the administratively-assigned hostname. Read-only: discovery collects it\nfrom the device, so neither create nor update accepts it.", "readOnly": true }, "sys_name_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysName. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_object_id": { "type": [ "string", "null" ], "description": "SNMP sysObjectID — the vendor's identifier for the device model." }, "sys_object_id_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysObjectID. Read-only: decided by whichever source read it.", "readOnly": true }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "virtualization_depth": { "type": "integer", "format": "int32", "description": "How many hosts sit above this one in its virtualization chain.", "readOnly": true, "minimum": 0 }, "virtualization_interface_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The interface on the virtualizing host that presents this host, for a network identity." }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." }, { "type": "null" } ] }, "virtualization_parent_host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The host this one runs under: the host of its virtualizing service.", "readOnly": true }, "virtualization_root_host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The host at the top of this host's virtualization chain, itself when it is the top.\n`null` when the host neither runs under nor runs another host.", "readOnly": true }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } }, "example": { "asset_tag": "IT-00412", "asset_tag_source": { "Probe": "Snmp" }, "chassis_id_source": "Unspecified", "created_at": "2026-01-15T10:30:00Z", "credential_assignments": [], "description": "Primary web server", "device_type": "Server", "display_name": "web-server-01", "display_name_rung": "Name", "firmware_revision": null, "firmware_revision_source": "Unspecified", "first_discovery_id": null, "hidden": false, "hostname": "web-server-01.local", "hostname_source": "Manual", "id": "550e8400-e29b-41d4-a716-446655440003", "interfaces": [ { "admin_status": "Up", "created_at": "2026-01-15T10:30:00Z", "display_name": "Uplink to Core Switch", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-44665544000f", "if_alias": "Uplink to Core Switch", "if_descr": "GigabitEthernet0/1", "if_index": 1, "if_name": "Gi0/1", "if_type": 6, "interface_source": { "Probe": "Snmp" }, "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "ip_configured": true, "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "mac_address_source": "ArpReply", "neighbor_candidates": [], "oper_status": "Up", "site_id": "550e8400-e29b-41d4-a716-446655440002", "speed_bps": 1000000000, "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "vlan_membership_source": "Unspecified" } ], "ip_addresses": [ { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440005", "ip_address": "192.168.1.100", "ip_address_source": "ArpReply", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "mac_address_source": "ArpReply", "name": "eth0", "position": 0, "site_id": "550e8400-e29b-41d4-a716-446655440002", "subnet_id": "550e8400-e29b-41d4-a716-446655440004", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "management_url_source": "Unspecified", "manufacturer_source": "Unspecified", "model_source": "Unspecified", "name": "web-server-01", "name_ladder": [ { "rung": "Name", "source": "Manual", "value": "web-server-01" }, { "rung": "Hostname", "source": "Manual", "value": "web-server-01.local" }, { "rung": "SysName", "source": null, "value": null }, { "rung": "ChassisId", "source": null, "value": null }, { "rung": "Address", "source": null, "value": "192.168.1.100" } ], "name_source": "Manual", "os": null, "os_source": "Unspecified", "ports": [ { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440006", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "number": 80, "port_source": "PortScan", "protocol": "Tcp", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "serial_number_source": "Unspecified", "services": [ { "bindings": [ { "created_at": "2026-10-22T23:24:30.797940Z", "first_discovery_id": null, "id": "1eb91317-7aa9-4574-9e4b-588aaf5d3fdf", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-10-22T23:24:30.797940Z", "lineage_id": null, "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Port", "updated_at": "2026-10-22T23:24:30.797940Z", "valid_from": "2026-10-22T23:24:30.797940Z", "valid_to": null } ], "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440007", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "nginx", "position": 0, "service_definition": "Consul", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_metadata": null, "virtualization_service_id": null } ], "site_id": "550e8400-e29b-41d4-a716-446655440002", "software_revision": null, "software_revision_source": "Unspecified", "source": { "type": "Manual" }, "sys_contact_source": "Unspecified", "sys_descr_source": "Unspecified", "sys_location_source": "Unspecified", "sys_name_source": "Unspecified", "sys_object_id_source": "Unspecified", "tags": [], "updated_at": "2026-01-15T10:30:00Z", "virtualization_depth": 0, "virtualization_interface_id": null, "virtualization_metadata": null, "virtualization_parent_host_id": null, "virtualization_root_host_id": null, "virtualization_service_id": null } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_IPAddress": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/IPAddressBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure.", "example": { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440005", "ip_address": "192.168.1.100", "ip_address_source": "ArpReply", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "mac_address_source": "ArpReply", "name": "eth0", "position": 0, "site_id": "550e8400-e29b-41d4-a716-446655440002", "subnet_id": "550e8400-e29b-41d4-a716-446655440004", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_InstallArtifacts": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Everything the UI needs to install (or reconfigure) a daemon, one field per install method so\neach is a first-class peer with its own content — no method is a special case bolted onto a\nlist. The binary methods are ready-to-paste commands (any api key is the [`API_KEY_PLACEHOLDER`],\nfilled in client-side); docker and msi carry their own structured content.", "required": [ "linux", "macos", "windows", "freebsd", "docker", "msi" ], "properties": { "docker": { "$ref": "#/components/schemas/DockerInstall", "description": "Container image reference." }, "freebsd": { "type": "string", "description": "Download for FreeBSD." }, "linux": { "type": "string", "description": "Download for Linux." }, "macos": { "type": "string", "description": "Download for macOS." }, "msi": { "$ref": "#/components/schemas/MsiInstall", "description": "Windows installer package." }, "windows": { "type": "string", "description": "Download for Windows." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Interface": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/InterfaceBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "display_name": { "type": [ "string", "null" ], "description": "What to call this interface when it has no `if_alias`/`if_descr`: the MAC it was\nidentified by, or \"Interface\" when it has neither.\n\nRead-only and computed from [`Interface::display_name`] — the same ladder topology port\nlabels an interface with, so it cannot be called one thing in a list and another on the\nmap. Set on every interface read from storage, so every endpoint that returns one carries\nit; absent on a daemon's own submission, and ignored on write.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_InterfaceNeighbours": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "One interface's resolved adjacencies plus the raw LLDP/CDP evidence behind them, for the\nhost editor's per-interface panel. Read on its own so that panel does not load the site's\nwhole topology bundle to show one port.", "required": [ "neighbours", "candidates" ], "properties": { "candidates": { "type": "array", "items": { "$ref": "#/components/schemas/InterfaceNeighborCandidate" }, "description": "Every distinct LLDP/CDP record heard on this interface, resolved or not." }, "neighbours": { "type": "array", "items": { "$ref": "#/components/schemas/InterfaceNeighborRow" }, "description": "Adjacencies resolved for this interface." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Invite": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/InviteBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_InvoiceBillingStatus": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Invoice billing state shown on the License tab.", "required": [ "bills_by_invoice" ], "properties": { "bills_by_invoice": { "type": "boolean", "description": "The subscription is billed by sent invoice." }, "open_invoice": { "oneOf": [ { "$ref": "#/components/schemas/InvoiceSummary" }, { "type": "null" } ] }, "pending_quote": { "oneOf": [ { "$ref": "#/components/schemas/PendingQuote" }, { "type": "null" } ] }, "po_number": { "type": [ "string", "null" ], "description": "Purchase order number printed on invoices." }, "written_off_invoice": { "oneOf": [ { "$ref": "#/components/schemas/InvoiceSummary", "description": "An invoice this customer defaulted on and we wrote off. While one\nstands, they cannot take payment terms again." }, { "type": "null" } ] } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_LicenseInstancesResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The server instances an organization's online key is running on.", "required": [ "instances", "usage" ], "properties": { "instances": { "type": "array", "items": { "$ref": "#/components/schemas/LicenseInstanceSummary" }, "description": "Every instance that has checked in, most recent first, including ones\nsilent long enough to no longer count." }, "usage": { "$ref": "#/components/schemas/InstanceUsage", "description": "Instances counting against the limit now, and the limit." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_LicenseKeyResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The result payload. Omitted on failure.", "required": [ "key", "key_type" ], "properties": { "key": { "type": "string", "description": "Signed key to set as `SCANOPY_LICENSE_KEY` on a self-hosted server." }, "key_type": { "$ref": "#/components/schemas/LicenseKeyType", "description": "Which key this is. An organization has one issued at a time." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_OnboardingStateResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response from onboarding state endpoint", "properties": { "org_name": { "type": [ "string", "null" ], "description": "Organization name from pending setup" }, "site": { "oneOf": [ { "$ref": "#/components/schemas/OnboardingSiteState", "description": "Site from pending setup (with name and ID)" }, { "type": "null" } ] }, "site_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Site ID from pending setup (if any)" }, "step": { "type": [ "string", "null" ], "description": "Current onboarding step (if any)" }, "use_case": { "oneOf": [ { "$ref": "#/components/schemas/UseCase", "description": "Use case selection (homelab, company, msp)" }, { "type": "null" } ] } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Option_SaveOfferCoupon": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "oneOf": [ { "type": "null" }, { "type": "object", "description": "Live terms for the configured save-offer coupon, read directly from\nStripe. Used by the cancel modal's Discount panel to render the offer\ndynamically instead of hard-coding the percent/duration.\n\nOnly returned when the coupon would actually catch the user's next\ninvoice — i.e. `next_renewal_at` falls within the coupon's `duration_in_months`\nwindow. Yearly subscribers partway through a cycle whose next renewal\nlands after the coupon's window get `None` from the endpoint and the\ncancel modal's Discount panel doesn't render.\n\n`billing_rate` lets the frontend pick monthly vs yearly copy: a monthly\nsubscriber thinks in terms of \"N months of discount\"; a yearly subscriber\nthinks in terms of \"my next renewal on {date}.\"", "required": [ "percent_off", "duration_in_months", "next_renewal_at", "billing_rate" ], "properties": { "billing_rate": { "$ref": "#/components/schemas/BillingRate", "description": "Billing interval the discount applies to." }, "duration_in_months": { "type": "integer", "format": "int64", "description": "How many months the discount lasts." }, "next_renewal_at": { "type": "string", "format": "date-time", "description": "When the discounted subscription next renews." }, "percent_off": { "type": "integer", "format": "int64", "description": "Discount applied by the retention offer." } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Organization": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/OrganizationBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Port": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/PortBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "Port entity with custom serialization that flattens PortType fields.", "example": { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440006", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "number": 80, "port_source": "PortScan", "protocol": "Tcp", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_ProvisionDaemonResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response from provisioning a daemon.\nContains the daemon record and the API key (shown only once).\n\nInstall commands are deliberately not here — fetch them from the install-command endpoint,\nwhich builds them idempotently and fills in this key. That keeps a display-only regenerate\n(advanced-setting change, OS switch) from re-minting the key.", "required": [ "daemon", "daemon_api_key" ], "properties": { "daemon": { "$ref": "#/components/schemas/DaemonResponse", "description": "The created daemon record (with version status)." }, "daemon_api_key": { "type": "string", "format": "password", "description": "The API key (plaintext) for daemon authentication.\nThis is shown only once - store it securely.", "readOnly": true } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_ProvisionEligibility": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Whether provisioning another daemon would be refused for the caller's organization.", "required": [ "email_verification_required" ], "properties": { "email_verification_required": { "type": "boolean", "description": "The organization already has a daemon and no owner has verified their email, so a new\ndaemon is refused until one does." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_PublicConfigResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The result payload. Omitted on failure.", "required": [ "server_port", "disable_registration", "disable_password_login", "oidc_providers", "discount_save_offer_available", "has_integrated_daemon", "has_email_service", "public_url", "needs_cookie_consent", "deployment_type", "non_production", "license_in_grace_period", "org_limit_reached", "server_admin_contact_email", "license_signing_available", "license_key_buffer_days", "license_key_grace_days" ], "properties": { "available_upgrade": { "oneOf": [ { "$ref": "#/components/schemas/AvailableUpgrade", "description": "The newest promoted release, when it is newer than this server. `None`\non cloud, with `SCANOPY_DISABLE_UPDATE_CHECK` set, before the first\ncheck answers, or when this server is current." }, { "type": "null" } ] }, "deployment_type": { "$ref": "#/components/schemas/DeploymentType", "description": "How this instance is run: cloud, commercial self-hosted, or community." }, "disable_password_login": { "type": "boolean", "description": "Whether email/password login is turned off, leaving OIDC as the only method." }, "disable_registration": { "type": "boolean", "description": "Whether self-service sign-up is turned off on this deployment." }, "discount_save_offer_available": { "type": "boolean", "description": "`STRIPE_SAVE_OFFER_COUPON_ID` env var is set. When false, the\ncancel modal hides the discount save-offer panel so the user\ndoesn't see an option the deployment can't fulfil." }, "has_email_service": { "type": "boolean", "description": "Whether outbound email is configured. Invites and password resets need it." }, "has_integrated_daemon": { "type": "boolean", "description": "Whether a daemon runs alongside the server, so no separate install is needed to start scanning." }, "license_entitlement_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When Scanopy Cloud last answered this instance's license check-in.\nOnline keys only." }, "license_expiry": { "type": [ "string", "null" ], "format": "date", "description": "Hard expiry — the drop-dead date after which the server rejects\nthe key. Referenced by the grace-period banner." }, "license_in_grace_period": { "type": "boolean", "description": "True when the license is past `intended_exp` but not yet past\nthe hard `exp` — the silent grace window." }, "license_instances": { "oneOf": [ { "$ref": "#/components/schemas/InstanceUsage", "description": "How many server instances this instance's online key is running on,\nagainst how many it covers, as Scanopy Cloud reported at the last\ncheck-in. Online keys only." }, { "type": "null" } ] }, "license_intended_expiry": { "type": [ "string", "null" ], "format": "date", "description": "User-visible expiry — the date displayed to end users under\nnormal operation. 7 days earlier than `license_expiry` for keys\nissued after grace-period support landed." }, "license_key_buffer_days": { "type": "integer", "format": "int32", "description": "Days past an organization's paid-through date before a license key\nreaches its user-visible expiry. Published so the UI shows the same\ndates the mint path bakes into keys, instead of its own copy.", "minimum": 0 }, "license_key_grace_days": { "type": "integer", "format": "int32", "description": "Further days past the user-visible expiry before a key stops working.", "minimum": 0 }, "license_key_type": { "oneOf": [ { "$ref": "#/components/schemas/LicenseKeyType", "description": "Whether the configured key is an offline key or an online key that\nfetches its entitlement from Scanopy Cloud. `None` when no key applies." }, { "type": "null" } ] }, "license_signing_available": { "type": "boolean", "description": "Whether this deployment can sign license keys. False on any server\nwithout a signing key, where the Settings License tab and the\nself-hosted plans would otherwise offer something the mint path\nrefuses. Reads the built issuer rather than the config value, since a\nkey can be present but unparseable." }, "license_status": { "oneOf": [ { "$ref": "#/components/schemas/LicenseStatusDiscriminants", "description": "Runtime state of the configured license key. `None` on deployments\nthat don't require one (community and cloud)." }, { "type": "null" } ] }, "license_valid_through": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the paid period, shown as \"Valid through\" and in the expiry\nbanners. Falls back to the intended expiry for keys minted without a\npaid-through date." }, "needs_cookie_consent": { "type": "boolean", "description": "Whether the client should show a cookie-consent prompt." }, "non_production": { "type": "boolean", "description": "Whether this server was started with `SCANOPY_NON_PRODUCTION`." }, "oidc_providers": { "type": "array", "items": { "$ref": "#/components/schemas/OidcProviderMetadata" }, "description": "Identity providers available on the login screen." }, "org_limit_reached": { "type": "boolean", "description": "True when this self-hosted instance has reached its licensed\norganization cap (`included_orgs`), so new-org registration is blocked.\nAlways false on cloud (multi-tenant) and on unlimited-org plans." }, "posthog_key": { "type": [ "string", "null" ], "description": "Public analytics key, when analytics is enabled." }, "public_url": { "type": "string", "format": "uri", "description": "Base URL this server is reachable at, as configured by the operator." }, "server_admin_contact_email": { "type": "string", "format": "email", "description": "Admin contact email to show users blocked by `org_limit_reached`,\nfrom `SCANOPY_SERVER_ADMIN_CONTACT_EMAIL`." }, "server_port": { "type": "integer", "format": "int32", "description": "Port this server listens on.", "minimum": 0 }, "snapshot_retention_days_override": { "type": [ "integer", "null" ], "format": "int32", "description": "`SCANOPY_SNAPSHOT_RETENTION_DAYS_OVERRIDE` if set on this instance.\nFrontend uses it inside the plan-comparison view to display the\neffective retention for this deployment rather than the per-plan\nfixture default.", "minimum": 0 }, "stripe_publishable_key": { "type": [ "string", "null" ], "description": "Stripe publishable key, exposed so the frontend can mount Stripe\nElements (Payment Element) for in-app card collection. `None` when\nbilling isn't configured. Publishable keys are safe to expose to the\nbrowser (same as `posthog_key`)." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_PublicShareMetadata": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Public share metadata (returned without authentication)", "required": [ "id", "name", "requires_password", "options", "enabled_views" ], "properties": { "enabled_views": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyView" }, "description": "Resolved list of available topology views for this share.\nFiltered by both share configuration and data availability.\nFirst element is the default view." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "name": { "type": "string", "description": "Human-facing name for this share." }, "options": { "$ref": "#/components/schemas/ShareOptions", "description": "What the viewer can see and do." }, "requires_password": { "type": "boolean", "description": "Whether a password must be supplied before the topology is returned." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_ServerCapabilities": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Server capabilities returned on startup/registration", "required": [ "server_version" ], "properties": { "deprecation_warnings": { "type": "array", "items": { "$ref": "#/components/schemas/DeprecationWarning" }, "description": "Deprecation warnings for the daemon" }, "last_unsupported_daemon_version": { "type": [ "string", "null" ], "description": "Daemons at or below this version are rejected by this server. `None` from a server too old\nto send it." }, "minimum_daemon_version": { "type": [ "string", "null" ], "description": "Deprecated: always 0.12.0. Daemons up to 0.18.2 require this field and only log it. Read\n`last_unsupported_daemon_version` instead." }, "server_version": { "type": "string", "description": "Server software version" } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Service": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/ServiceBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure.", "example": { "bindings": [ { "created_at": "2026-10-22T23:24:30.827090Z", "first_discovery_id": null, "id": "c87c9317-ad74-4576-ba13-b477778df3a4", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-10-22T23:24:30.827090Z", "lineage_id": null, "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Port", "updated_at": "2026-10-22T23:24:30.827090Z", "valid_from": "2026-10-22T23:24:30.827090Z", "valid_to": null } ], "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440007", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "nginx", "position": 0, "service_definition": "Consul", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_metadata": null, "virtualization_service_id": null } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_SetupIntentResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response for creating a SetupIntent — the client secret the frontend\nPayment Element uses to collect and confirm a card in-app.", "required": [ "client_secret" ], "properties": { "client_secret": { "type": "string", "description": "Stripe SetupIntent client secret, used to mount the Payment Element." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_SetupResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response from setup endpoint", "properties": { "site_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Pre-assigned id of the site created at registration, if one was requested." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Share": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/ShareBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_ShareAccessTokenResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Access token returned after successful password verification.\n\nThe token is an HS256 JWT tied to the share's `password_hash` — changing\nthe share password implicitly invalidates all outstanding tokens.", "required": [ "access_token", "expires_at" ], "properties": { "access_token": { "type": "string", "description": "Bearer token granting access to this share for the rest of the session." }, "expires_at": { "type": "string", "format": "date-time", "description": "When this record stops being valid." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_ShareWithTopology": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Share with topology data (returned after authentication/verification).\n\nReturns the slim topology row (`{ id, site_id, options }`) plus the\n`TopologyData` bundle (entities + the per-view graph built on request). The\nshare viewer composes these with the same `toRenderableTopology` the app\nuses — no server-side merge.", "required": [ "share", "topology", "data", "export_features" ], "properties": { "data": { "$ref": "#/components/schemas/TopologyData", "description": "Entities and graph for the requested view." }, "export_features": { "$ref": "#/components/schemas/ExportFeatures", "description": "Which exports the share creator's plan allows." }, "share": { "$ref": "#/components/schemas/PublicShareMetadata", "description": "Public metadata for the share itself." }, "topology": { "$ref": "#/components/schemas/Topology", "description": "The shared topology record." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Site": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/SiteBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "effective_stale_after_hours": { "type": "integer", "format": "int64", "description": "`stale_after_hours` with the server's default already applied.\n\nComputed, never stored (excluded from `to_params`). Published so the\nfrontend derives staleness from the *same* number the digest uses rather\nthan re-declaring the default in TypeScript, where the two could drift\nand a host could read stale in the app but current in the digest email.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure.", "example": { "created_at": "2026-01-15T10:30:00Z", "credential_ids": [], "effective_stale_after_hours": 672, "id": "550e8400-e29b-41d4-a716-446655440002", "name": "Home Site", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "stale_after_hours": null, "tags": [], "updated_at": "2026-01-15T10:30:00Z" } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Snapshot": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/SnapshotBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_String": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "string", "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Subnet": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/SubnetBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure.", "example": { "cidr": "192.168.1.0/24", "cidr_source": "DaemonSelfReport", "created_at": "2026-01-15T10:30:00Z", "description": "Local area network", "first_discovery_id": null, "id": "550e8400-e29b-41d4-a716-446655440004", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "LAN", "name_source": "Unspecified", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "subnet_type": "Lan", "subnet_type_source": "Unspecified", "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_service_id": null, "vlan_ids": [] } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_SubnetResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/Subnet" }, { "type": "object", "required": [ "used_addresses", "usable_addresses", "nesting_depth", "nesting_root_id", "range_evidence" ], "properties": { "nesting_depth": { "type": "integer", "format": "int32", "description": "How many ranges this one sits inside: 0 for a range nested in nothing.", "readOnly": true, "minimum": 0 }, "nesting_root_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The outermost range of the tree this one sits in: itself when it holds others and sits in\nnone. `null` for a range that nests with nothing.", "readOnly": true }, "parent_subnet_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most specific other subnet on the same site whose range contains this one. `null` when\nnone does. Children are the subnets whose parent is this one.", "readOnly": true }, "range_evidence": { "oneOf": [ { "$ref": "#/components/schemas/RangeEvidence", "description": "For a range Scanopy assumed from what LLDP/CDP neighbours published, the far ends it was\ndrawn around and the devices that saw them. `null` for every other range, and for an\nassumed one no scan created." }, { "type": "null" } ], "readOnly": true }, "usable_addresses": { "type": "integer", "format": "int64", "description": "Addresses a host can hold in this range. Saturates for IPv6 ranges wider than a `/64`.", "readOnly": true, "minimum": 0 }, "used_addresses": { "type": "integer", "format": "int64", "description": "Distinct addresses held in this range, including those filed in subnets nested inside it.", "readOnly": true, "minimum": 0 } } } ], "description": "A subnet together with what is derived from the rest of its site." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Tag": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/TagBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure.", "example": { "color": "Green", "created_at": "2026-01-15T10:30:00Z", "description": "Production environment resources", "icon": null, "id": "550e8400-e29b-41d4-a716-44665544000a", "lineage_id": null, "name": "production", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "position": 1, "tag_group_id": "550e8400-e29b-41d4-a716-446655440010", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_TagGroup": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/TagGroupBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "A named set of tags of which an entity holds at most one, in an order the organization sets,\noptionally limited to some entity types.", "example": { "created_at": "2026-01-15T10:30:00Z", "description": "Which stage of delivery an asset serves.", "entity_types": null, "group_type": "Custom", "id": "550e8400-e29b-41d4-a716-446655440010", "name": "Environment", "ordered": true, "organization_id": "550e8400-e29b-41d4-a716-446655440001", "scope_computed": false, "updated_at": "2026-01-15T10:30:00Z" } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_TestReachabilityResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response from a reachability test.", "required": [ "reachable" ], "properties": { "error": { "type": [ "string", "null" ], "description": "Error message if not reachable" }, "health": { "type": [ "boolean", "null" ], "description": "Health check result (only present when check_health was true)" }, "reachable": { "type": "boolean", "description": "Whether the TCP connection succeeded" } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Topology": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/TopologyBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_TopologyData": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Bundle of entities + the built graph that feed the topology render, export,\nand share pipelines.\n\nLoaded by [`crate::server::topology::service::main::TopologyService::get_topology_data`]\nfor either the live view (`snapshot_id = None`) or a point-in-time snapshot\n(`snapshot_id = Some(id)`). The per-view `nodes`/`edges` are built on request\nfrom these entities + the site's grouping options\n(`build_view_graphs`) — they are not persisted. The frontend selects the\nactive view's slice client-side.", "required": [ "hosts", "ip_addresses", "subnets", "dependencies", "ports", "bindings", "interfaces", "services", "vlans", "tags", "application_group_id" ], "properties": { "application_group_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The organization's Application tag group. Its tags place entities in applications.\nShipped here because share and embed viewers can't list tag groups." }, "available_views": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyView" }, "description": "Views whose data is present in this entity set (L3/Workloads always;\nL2 Physical iff LLDP/CDP neighbors exist; Application iff app-flagged\ntags are used). The topology tab restricts a snapshot's view picker to\nthese — you can't set up SNMP or create app tags on a historical\nsnapshot — while the live view shows all views with setup prompts." }, "bindings": { "type": "array", "items": { "$ref": "#/components/schemas/Binding" }, "description": "Service bindings included in this topology." }, "dependencies": { "type": "array", "items": { "$ref": "#/components/schemas/Dependency" }, "description": "Dependencies included in this topology." }, "edges": { "type": "object", "description": "Connections between the nodes of the built graph.", "additionalProperties": { "type": "array", "items": { "$ref": "#/components/schemas/Edge" } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } }, "filtered_out": { "type": "object", "description": "How many entities of each type a server-side metadata filter removed, by the filter that\nremoved them.\n\nServer-filtered entities never reach the browser, so this is the only way the frontend can\nsay \"171 interfaces hidden by By link\" rather than presenting an empty view as an empty\nsite. Keyed by entity and filter only, with no view: the hide-set is per view but a drop\nis not — an entity is removed from the one shared bundle only when *every* view that could\nrender it hides it (see `metadata_filter`).\n\nEmpty when nothing was filtered, which is the common case.", "additionalProperties": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 0 }, "propertyNames": { "type": "string", "description": "The type of metadata filter. One variant per conceptually-distinct filter\nacross the app — Category (on Service), Virtualization (on Host), and so\non. Kept narrow on purpose: adding a new filter means adding a variant\nhere + a `HasFilterValues` impl on the relevant entity.", "enum": [ "Category", "Virtualization", "LinkState", "Staleness", "OperStatus", "GuestState" ] } }, "propertyNames": { "type": "string", "enum": [ "Site", "Vlan", "Subnet", "Host", "Service", "IPAddress", "Interface", "Port", "Binding", "Discovery", "Daemon", "DaemonApiKey", "Tag", "TagGroup", "User", "UserApiKey", "Credential", "Organization", "Invite", "Topology", "Snapshot", "Share", "Dependency" ] } }, "hosts": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyHost" }, "description": "Hosts included in this topology, each carrying the title the map and the host list share." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyInterface" }, "description": "Interfaces included in this topology, each carrying its metadata filter values." }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "IP addresses included in this topology." }, "neighbours": { "type": "array", "items": { "$ref": "#/components/schemas/InterfaceNeighborRow" }, "description": "Resolved LLDP/CDP neighbour adjacencies (GH #701): the merged read-model of\n`interface_neighbor_interfaces` + `interface_neighbor_hosts`, replacing the old\n`Interface.neighbor` scalar now that a port can anchor several links." }, "nodes": { "type": "object", "description": "Per-view graph built on request from the entities above + grouping\noptions. Keyed by view so switching the active perspective is a\nclient-side slice selection.", "additionalProperties": { "type": "array", "items": { "$ref": "#/components/schemas/Node" } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/Port" }, "description": "Ports included in this topology." }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyService" }, "description": "Services included in this topology, each carrying its metadata filter values." }, "subnets": { "type": "array", "items": { "$ref": "#/components/schemas/Subnet" }, "description": "Subnets included in this topology." }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/Tag" }, "description": "Tags assigned to this entity." }, "vlans": { "type": "array", "items": { "$ref": "#/components/schemas/Vlan" }, "description": "VLANs included in this topology." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_UnseenReleaseNotes": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The releases the caller hasn't dismissed.", "required": [ "releases", "auto_open" ], "properties": { "auto_open": { "type": "boolean", "description": "Whether the modal opens on its own: true when any unseen release asks\nfor it, including ones left out for the changelog." }, "changelog_url": { "type": [ "string", "null" ], "description": "The full changelog, present when older unseen releases were left out." }, "releases": { "type": "array", "items": { "$ref": "#/components/schemas/ReleaseNoteResponse" }, "description": "Newest first. Empty when there is nothing new." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_UpgradeArtifacts": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Everything the UI needs to upgrade a daemon: the binary steps per OS, and the docker method.", "required": [ "linux", "macos", "windows", "freebsd", "docker", "legacy_service_name" ], "properties": { "docker": { "$ref": "#/components/schemas/DockerUpgrade" }, "freebsd": { "type": "array", "items": { "$ref": "#/components/schemas/UpgradeStep" }, "description": "Binary upgrade steps on FreeBSD." }, "legacy_service_name": { "type": "boolean", "description": "The restart step names the service after the daemon, as installs from before the install\nengine were named. Such a daemon may have been set up differently, so the UI points the\noperator to `scanopy-daemon list` for the real service." }, "linux": { "type": "array", "items": { "$ref": "#/components/schemas/UpgradeStep" }, "description": "Binary upgrade steps on Linux." }, "macos": { "type": "array", "items": { "$ref": "#/components/schemas/UpgradeStep" }, "description": "Binary upgrade steps on macOS." }, "windows": { "type": "array", "items": { "$ref": "#/components/schemas/UpgradeStep" }, "description": "Binary upgrade steps on Windows." } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_User": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/UserBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_UserApiKey": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/UserApiKeyBase" }, { "type": "object", "required": [ "id", "updated_at", "created_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_UserApiKeyResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response for user API key creation/rotation\nContains the full API key record plus the plaintext key (shown only once)", "required": [ "api_key", "key" ], "properties": { "api_key": { "$ref": "#/components/schemas/UserApiKey", "description": "The stored key record." }, "key": { "type": "string", "format": "password", "description": "The plaintext API key - only returned once during creation or rotation", "readOnly": true } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Vec_BillingPlan": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "oneOf": [ { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Community" ] } } } ], "title": "Community" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Free" ] } } } ], "title": "Free" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Starter" ] } } } ], "title": "Starter" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Pro" ] } } } ], "title": "Pro" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Team" ] } } } ], "title": "Team" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Business" ] } } } ], "title": "Business" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Enterprise" ] } } } ], "title": "Enterprise" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Demo" ] } } } ], "title": "Demo" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "CommercialSelfHosted" ] } } } ], "title": "CommercialSelfHosted" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "SelfHostedStandard" ] } } } ], "title": "SelfHostedStandard" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "SelfHostedPlus" ] } } } ], "title": "SelfHostedPlus" } ] }, "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Vec_Credential": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/CredentialBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "is_default" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "is_default": { "type": "boolean", "description": "Whether this is the organization's seeded default for its type: identical to the\ncredential every organization starts with. A default can't be deleted, and its settings\ncan't change. Computed from `credential_type`, so a value sent in a request is ignored.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Vec_Discovery": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/DiscoveryBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "integration_targets" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "force_full_scan": { "type": "boolean", "description": "When true, the next scan will be a full port scan regardless of interval" }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "integration_targets": { "type": "array", "items": { "$ref": "#/components/schemas/IntegrationTarget" }, "description": "Per-daemon integration targeting: which integrations run on this daemon, and on which\nIPs. Delivered via the init command at registration and editable via the discovery\nmodal. This is the single home for cred↔IP targeting; it replaces the global\n`credential.target_ips` (race-prone, consumed once).\n\nOne-shot: a target is offered to the daemon until a scan completes successfully, then\ndropped by [`Discovery::apply_successful_scan`]. Credentials that earned a durable home\nduring the scan keep being retried from there — `host_credentials` for one that probed\nsuccessfully, `site_credentials` for a broadcast one (see\n[`Discovery::take_site_scope_credential_ids`])." }, "next_run_is_full_scan": { "type": "boolean", "description": "Whether the next run of this discovery will be a full scan of all 65,535 ports rather than\na light scan of common ports. Computed by the server on read and never stored.", "readOnly": true }, "scan_count": { "type": "integer", "format": "int32", "description": "Number of completed scans (incremented by server on session completion)", "readOnly": true, "minimum": 0 }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Vec_DiscoveryUpdatePayload": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "type": "object", "description": "Progress update from daemon to server during discovery", "required": [ "session_id", "daemon_id", "site_id", "phase", "discovery_type", "progress" ], "properties": { "credential_results": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialRunResult" }, "description": "What each stored credential did in this run, one entry per credential. Results, not\nproblems: those are `warnings`. Old servers ignore it.\n\nRead per entry, like `warnings`: an outcome shape from a newer daemon becomes `Unknown`\nfor that credential instead of failing the terminal payload." }, "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon this entity refers to." }, "daemon_version": { "type": [ "string", "null" ], "description": "The daemon's version when the run ended. Stamped by the server at terminal, so it\nsurvives the daemon being deleted or upgraded." }, "discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery configuration this session belongs to.\nAlways enriched server-side; daemons do not send this field." }, "discovery_type": { "$ref": "#/components/schemas/DiscoveryType", "description": "What type of discovery is running." }, "error": { "type": [ "string", "null" ], "description": "Failure message, when the run did not complete." }, "estimated_remaining_secs": { "type": [ "integer", "null" ], "format": "int32", "description": "Rough estimate of the time left, in seconds.", "minimum": 0 }, "finished_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the run finished. `null` while it is still going." }, "hosts_discovered": { "type": [ "integer", "null" ], "format": "int32", "description": "Hosts found so far.", "minimum": 0 }, "last_update_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the server last heard about this run. Stamped by the server at terminal." }, "phase": { "$ref": "#/components/schemas/DiscoveryPhase", "description": "Which stage of the run is in progress." }, "progress": { "type": "integer", "format": "int32", "description": "Completion of the current phase, from 0 to 1.", "minimum": 0 }, "reason": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryTerminalReason", "description": "Why the run ended. Set on terminal payloads; absent on runs recorded before it existed.\nA daemon may send one of the reasons only it can know; the server assigns every other.\nA reason this server doesn't know reads as `None`, and the server assigns one from the\nphase." }, { "type": "null" } ] }, "scanned": { "oneOf": [ { "$ref": "#/components/schemas/ScannedEntityIds", "description": "Canonical IDs of entities scanned in this session, populated daemon-\nside at terminal. **Transient**: stripped at `SqlValue::RunType` bind\ntime so it doesn't persist into the historical Discovery row's JSONB.\nAvailable in-memory through the `EntityOperation::Created` event\npublished for the historical Discovery row (the event scope carries\n`Entity::Discovery(...)`, the full in-memory struct), where per-entity\nFK-update subscribers consume it.\n\n`Some(...)` when the daemon is sending the terminal payload over the\nwire. `None` when read back from a persisted historical row, or when\nnot yet set." }, { "type": "null" } ] }, "session_id": { "type": "string", "format": "uuid", "description": "The discovery run this update belongs to." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "started_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the run started." }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/DiscoveryWarning" }, "description": "Non-fatal findings from a completed run — one per occurrence, each carrying the code that\nidentifies it and the detail that fills the sentence. Unlike `error`, these do not mark the\nrun failed.\n\nRead through [`deserialize_warnings`] rather than the derived impl, which is what keeps\nhistorical records and pre-coded daemons rendering: both send bare strings here, and both\nland as `Unknown` carrying that text instead of failing the whole payload." } } }, "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Vec_GroupCount": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "type": "object", "description": "Size of one group in a grouped list, across every page.", "required": [ "count" ], "properties": { "count": { "type": "integer", "format": "int64", "description": "How many rows fall in this group in total, not just on this page.", "minimum": 0 }, "value": { "type": [ "string", "null" ], "description": "The group's value, rendered as text. `null` for rows whose group key is\nNULL (the \"ungrouped\" bucket)." } } }, "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Vec_Invite": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/InviteBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Vec_Tag": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/TagBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "color": "Green", "created_at": "2026-01-15T10:30:00Z", "description": "Production environment resources", "icon": null, "id": "550e8400-e29b-41d4-a716-44665544000a", "lineage_id": null, "name": "production", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "position": 1, "tag_group_id": "550e8400-e29b-41d4-a716-446655440010", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_VersionInfo": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Version information for API compatibility checking", "required": [ "api_version", "server_version" ], "properties": { "api_version": { "type": "integer", "format": "int32", "description": "Current API version (integer, increments on breaking changes)", "minimum": 0 }, "min_compatible_client": { "type": [ "string", "null" ], "description": "Minimum client version that can use this API (optional, for future use)" }, "server_version": { "type": "string", "description": "Server version (semver)", "example": "0.12.10" } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_Vlan": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/VlanBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_VlanDiscoveryResponse": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response for discovery upsert", "required": [ "vlans" ], "properties": { "vlans": { "type": "array", "items": { "$ref": "#/components/schemas/VlanDiscoveryResponseItem" }, "description": "Mapping of vlan_number → VLAN entity UUID" } } }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "ApiResponse_u32": { "type": "object", "description": "Envelope for a successful response. Failures are sent with a non-2xx status\nand an `ApiErrorResponse` body instead.", "required": [ "success", "meta" ], "properties": { "data": { "type": "integer", "format": "int32", "description": "The result payload. Omitted on failure.", "minimum": 0 }, "error": { "type": [ "string", "null" ], "description": "Not sent on a successful response. Failure messages arrive in an\n`ApiErrorResponse`." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "Always `true` on a successful response." } } }, "AttributeSource": { "oneOf": [ { "type": "string", "description": "Provenance unknown: a payload from a daemon predating the rung, a row predating the column,\nor a source this binary does not recognise. Below everything, so it never displaces a value\nwe know the origin of and anything may improve on it.", "enum": [ "Unspecified" ] }, { "type": "string", "description": "The host's own address, standing in for a name it does not have. Nothing on this server\nwrites it any more: an address is an identifier, and the display ladder shows it without a\ncopy in `name`. Daemons up to v0.17.14 still send it, and it is kept so their payloads\ndeserialise before ingest drops the copy.", "enum": [ "OwnAddress" ] }, { "type": "string", "description": "Implied by a matched service definition.", "enum": [ "ServiceMatch" ] }, { "type": "string", "description": "A /24 (or /64) convention around an address a neighbour advertised. LLDP carries no netmask\nTLV, so the range is our invention rather than the neighbour's claim.", "enum": [ "LldpNeighbourAddress" ] }, { "type": "string", "description": "A manufacturer string synthesised from a numeric CIP vendor ID, because we ship no ODVA\nvendor table. Distinct from [`Self::Probe`] over the same transport for the reason the\nmodule doc gives: the variant names what was read, not how it arrived, and `\"CIP vendor 1\"`\nis our own construction rather than anything the device emitted.", "enum": [ "CipVendorId" ] }, { "type": "string", "description": "An operating system matched in SNMP `sysDescr` against a Recog fingerprint. The text is the\ndevice's own, but the OS is our reading of it, so it ranks below anything read off the host.", "enum": [ "SysDescrMatch" ] }, { "type": "string", "description": "An operating system implied by an SNMP `sysObjectID` a Recog fingerprint names. Inferred for\nthe same reason as [`Self::SysDescrMatch`].", "enum": [ "SysObjectIdMatch" ] }, { "type": "string", "description": "An operating system matched in the system description an LLDP neighbour advertised for itself\n(`lldpRemSysDesc`). Inferred for the same reason as [`Self::SysDescrMatch`].", "enum": [ "LldpSysDescMatch" ] }, { "type": "string", "description": "An operating system matched in an SSH server's identification string, such as the\n`Ubuntu-3ubuntu13` a distribution's OpenSSH package appends.", "enum": [ "SshBannerMatch" ] }, { "type": "string", "description": "An operating system matched in an HTTP `Server` header, such as `Microsoft-IIS/10.0`.", "enum": [ "HttpServerMatch" ] }, { "type": "string", "description": "An operating system matched in an mDNS `_device-info._tcp` record, such as a Mac's\n`model=MacBookPro18,3`.", "enum": [ "DnsSdDeviceInfoMatch" ] }, { "type": "string", "description": "An operating system matched in the `model` an mDNS `_airplay._tcp` record carries, such as a\nMac's `model=Mac17,2`. Macs that share nothing still advertise AirPlay.", "enum": [ "DnsSdAirPlayMatch" ] }, { "type": "string", "description": "A subnet type guessed from the name of the interface that holds the range: `wg0` for a VPN\ntunnel, `br-guest` for a guest network. The name is the operating system's; the type is our\nreading of it.", "enum": [ "InterfaceNameMatch" ] }, { "type": "string", "description": "A DNS-SD instance label — the Chromecast `fn=Living Room TV`, typed by a person during\ndevice setup.", "enum": [ "DnsSdInstanceName" ] }, { "type": "string", "description": "A hostname a device announced for itself over mDNS: the SRV target, such as\n`chromecast-a1b2c3.local`. Machine-generated, unlike the instance label beside it.", "enum": [ "DnsSdHostname" ] }, { "type": "string", "description": "A chassis ID a neighbour advertised for itself.", "enum": [ "LldpChassisId" ] }, { "type": "string", "description": "An LLDP neighbour the `lldpd` service on the daemon's own host heard on one of its ports.\nDistinct from [`Self::LldpChassisId`], which is the identity a neighbour advertised: this\nnames the listener that recorded the adjacency.", "enum": [ "Lldpd" ] }, { "type": "string", "description": "Reverse DNS. A known speaker that is not the subject.", "enum": [ "ReverseDns" ] }, { "type": "string", "description": "A router's `ipNetToMediaTable` cache, or a switch's bridge FDB. One variant for both: each\nis \"a device telling us about a MAC that is not its own\", with no basis to rank one above\nthe other.", "enum": [ "ForwardingTable" ] }, { "type": "string", "description": "We broadcast an ARP request for a specific address and something replied claiming it.", "enum": [ "ArpReply" ] }, { "type": "string", "description": "We sent an ICMP echo request to the address and it answered.", "enum": [ "IcmpEcho" ] }, { "type": "string", "description": "We opened a TCP connection to a port, or a UDP probe to it was answered. Stamps the port,\nand the address of a host found only this way.", "enum": [ "PortScan" ] }, { "type": "string", "description": "The daemon reading its own host: NIC and routing configuration, its own hostname and its\nOS. `Native` for the reason [`Self::ProfinetDcp`] is: the host describes itself, first-hand,\nwith no transport or third party between the reading and the machine. It outranks\n[`Self::ContainerRuntimeInfo`], whose engine may run in a VM on this machine.", "enum": [ "DaemonSelfReport" ] }, { "type": "string", "description": "A PROFINET DCP Identify exchange: we sent the request and the transport correlated the\nreply to the station that sent it. `Native`, not merely `Queried` — like [`Self::Probe`]\nover [`ClientProbe::ModbusTcp`]/`EtherNetIp`/`OpcUa`, the protocol is the device's own\nrather than a generic MIB approximating it. A bare variant rather than `Probe(ClientProbe)`\non purpose: [`ClientProbe`] is scoped to application probes reached over an already-open\nTCP/UDP port (`every_client_probe_variant_has_a_producer` enforces that every variant has an\n`AppProbe` producer), and DCP is raw Ethernet with no port at all.", "enum": [ "ProfinetDcp" ] }, { "type": "string", "description": "A container engine (Docker, Podman) reporting the machine it runs on, from its `info`\nendpoint: the host OS, its release and kernel. `Queried`, not `Native`: we chose the engine\nand authenticated to it, but the machine it describes is the host only when the engine runs\non it directly. Docker Desktop, OrbStack, Colima, Rancher Desktop and `podman machine` run\nthe engine in a Linux VM, and `info` describes that VM. A bare variant rather than\n`Probe(Docker)`, which is the container listing; this is a different exchange.", "enum": [ "ContainerRuntimeInfo" ] }, { "type": "string", "description": "The configuration of the hypervisor or container runtime that runs the guest, read\nthrough its API: the MAC of a Proxmox guest's virtual NIC or of a container's macvlan\nendpoint, and the hostname and distribution a Proxmox LXC container's config sets. `Native`\nfor the reason [`Self::ProfinetDcp`] is: the hypervisor or runtime assigned that MAC, keeps\nit for the guest's or container's life and puts it on the wire for it, so its record is the NIC's own identity, not a third party's report of it.\nThat is what lets a guest with no address still be identified by its NIC. A bare variant\nrather than `Probe(Proxmox)` or `Probe(Docker)`, which cover everything else those APIs\nreport and stay `Reported`.", "enum": [ "HypervisorConfig" ] }, { "type": "string", "description": "Software inside a guest read this from the guest's own operating system and answered the\nhypervisor's API: the QEMU guest agent behind Proxmox, and the same role VMware Tools and\nHyper-V integration services fill. `Native` for the reason [`Self::ProfinetDcp`] is: the\nguest describes itself, over a channel the hypervisor binds to that one guest, so it is\nfirst-hand, not the hypervisor's report of the guest (`Probe(Proxmox)`).", "enum": [ "GuestAgent" ] }, { "type": "string", "description": "A script the operator wrote, run on the host over SSH, printed it. Queried: we chose the\nhost, authenticated to it and read the answer from its own shell. Human-authored, because\nthe operator chose what the script reports, which is what puts it above the machine values\nSNMP and LLDP emit about the same fields.", "enum": [ "SshScript" ] }, { "type": "object", "title": "Probe", "description": "A value the thing emitted about itself, over whatever transport [`ClientProbe`] names.", "required": [ "Probe" ], "properties": { "Probe": { "$ref": "#/components/schemas/ClientProbe", "description": "A value the thing emitted about itself, over whatever transport [`ClientProbe`] names." } } }, { "type": "object", "title": "Authored", "description": "A value a person entered into the thing we read it from, carried back over the same\ntransport: SNMP `sysLocation`, a name set in a controller.", "required": [ "Authored" ], "properties": { "Authored": { "$ref": "#/components/schemas/ClientProbe", "description": "A value a person entered into the thing we read it from, carried back over the same\ntransport: SNMP `sysLocation`, a name set in a controller." } } }, { "type": "string", "description": "A person asserted it in Scanopy. Nothing discovery reads displaces it.", "enum": [ "Manual" ] } ], "description": "How a discovered value reached us.\n\nEvery variant here has something that writes it. Sources with no producer are deliberately\nabsent: a rung the applier can never see is a rung nothing can be tested against, and the tier\ndecision is better made by whoever writes the reader than guessed in advance.\n\n**Do not derive `Ord`.** A derive compares the variant and then the payload, which would make\n`Probe(Docker) < Probe(Snmp)` on declaration order alone. Ordering is only ever [`Self::rank`].\n\n**Externally tagged**, which is serde's default and is left unstated for that reason. Thirty-three\nof these variants carry nothing, and a tag on a variant with no content is a key that distinguishes\nnothing — `\"Unspecified\"`, not `{\"type\":\"Unspecified\"}`, in every row of every column. The two\nthat do carry a [`ClientProbe`] keep it under their own name, `{\"Probe\":\"Snmp\"}`, which tells them\napart from each other and from every bare name." }, "AvailableUpgrade": { "type": "object", "description": "A newer release than the one this server runs.", "required": [ "version", "release_notes_url" ], "properties": { "release_notes_url": { "type": "string", "format": "uri", "description": "That release's page on the public changelog." }, "version": { "type": "string", "description": "Semver of the newest promoted release, without a leading `v`." } } }, "BillingPlan": { "oneOf": [ { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Community" ] } } } ], "title": "Community" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Free" ] } } } ], "title": "Free" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Starter" ] } } } ], "title": "Starter" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Pro" ] } } } ], "title": "Pro" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Team" ] } } } ], "title": "Team" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Business" ] } } } ], "title": "Business" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Enterprise" ] } } } ], "title": "Enterprise" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Demo" ] } } } ], "title": "Demo" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "CommercialSelfHosted" ] } } } ], "title": "CommercialSelfHosted" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "SelfHostedStandard" ] } } } ], "title": "SelfHostedStandard" }, { "allOf": [ { "$ref": "#/components/schemas/PlanConfig" }, { "type": "object", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "SelfHostedPlus" ] } } } ], "title": "SelfHostedPlus" } ] }, "BillingRate": { "type": "string", "enum": [ "Month", "Year" ] }, "Binding": { "allOf": [ { "$ref": "#/components/schemas/BindingBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "Association between a service and a port / interface that the service is listening on", "example": { "created_at": "2026-10-22T23:24:30.798643Z", "first_discovery_id": null, "id": "f2f2a59d-eea4-4275-840d-c0c7d18a5bc4", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-10-22T23:24:30.798643Z", "lineage_id": null, "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Port", "updated_at": "2026-10-22T23:24:30.798643Z", "valid_from": "2026-10-22T23:24:30.798643Z", "valid_to": null } }, "BindingBase": { "allOf": [ { "$ref": "#/components/schemas/BindingType", "description": "What the service is bound to — a port, or an IP address on its own." }, { "type": "object", "required": [ "service_id", "site_id" ], "properties": { "service_id": { "type": "string", "format": "uuid", "description": "The service this entity refers to." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." } } } ], "description": "The base data for a Binding entity (everything except id, created_at, updated_at)" }, "BindingInput": { "oneOf": [ { "type": "object", "title": "IPAddress", "description": "Bind to an interface (service is present at this interface without a specific port)", "required": [ "id", "ip_address_id", "type" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Client-provided UUID for this binding" }, "ip_address_id": { "type": "string", "format": "uuid", "description": "The IP address the service is present at." }, "type": { "type": "string", "enum": [ "IPAddress" ] } } }, { "type": "object", "title": "Port", "description": "Bind to a port (optionally on a specific ip_address)", "required": [ "id", "port_id", "type" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Client-provided UUID for this binding" }, "ip_address_id": { "type": [ "string", "null" ], "format": "uuid", "description": "null = bind to all ip_addresses" }, "port_id": { "type": "string", "format": "uuid", "description": "The port the service listens on." }, "type": { "type": "string", "enum": [ "Port" ] } } } ], "description": "Input for creating or updating a binding within a service.\nUsed in both CreateHostRequest and UpdateHostRequest.\nClient must provide a UUID for the binding." }, "BindingType": { "oneOf": [ { "type": "object", "title": "IPAddress", "description": "IP address binding: Service is present at an IP address without a specific port.\nUsed for non-port-bound services like gateways. Conflicts with port bindings on the same IP address.", "required": [ "ip_address_id", "type" ], "properties": { "ip_address_id": { "type": "string", "format": "uuid", "description": "The IP address the service is present at." }, "type": { "type": "string", "enum": [ "IPAddress" ] } } }, { "type": "object", "title": "Port", "description": "Port binding: Service listens on a specific port, optionally on a specific IP address.\nIf `ip_address_id` is `null`, the service listens on this port across all IP addresses,\nwhich supersedes any specific-IP-address bindings for the same port.", "required": [ "port_id", "ip_address_id", "type" ], "properties": { "ip_address_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The IP address this port binding applies to. If `null`, the binding applies to all\nIP addresses on the host (and supersedes specific-IP-address bindings for this port)." }, "port_id": { "type": "string", "format": "uuid", "description": "The port the service listens on." }, "type": { "type": "string", "enum": [ "Port" ] } } } ], "description": "The type of binding - either to an interface or to a port.\n\nBindings associate a service with network resources (ip_addresses/ports) on a host.\n\n## Validation Rules\n\n- All bindings must reference ports/interfaces that belong to the same host as the service.\n- Interface bindings conflict with port bindings on the same interface.\n- A port binding on all ip_addresses (`ip_address_id: null`) conflicts with any interface binding.\n- When a port binding with `ip_address_id: null` is created, it supersedes (removes) any\n existing specific-interface bindings for the same port." }, "BulkDeleteResponse": { "type": "object", "required": [ "deleted_count", "requested_count" ], "properties": { "deleted_count": { "type": "integer", "description": "How many records were actually deleted.", "minimum": 0 }, "requested_count": { "type": "integer", "description": "How many IDs the request asked to delete.", "minimum": 0 } } }, "BulkSetEnabledRequest": { "type": "object", "required": [ "ids", "enabled" ], "properties": { "enabled": { "type": "boolean", "description": "Whether their schedules run." }, "ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "IDs of the scheduled discoveries." } } }, "BulkTagRequest": { "type": "object", "description": "Request body for bulk tag operations", "required": [ "entity_type", "entity_ids", "tag_id" ], "properties": { "entity_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The IDs of entities to modify" }, "entity_type": { "$ref": "#/components/schemas/EntityDiscriminants", "description": "The entity type (e.g., Host, Service, Subnet)" }, "tag_id": { "type": "string", "format": "uuid", "description": "The tag ID to add or remove" } } }, "BulkTagResponse": { "type": "object", "description": "Response for bulk tag operations", "required": [ "affected_count" ], "properties": { "affected_count": { "type": "integer", "description": "Number of entities affected", "minimum": 0 } } }, "CancelReason": { "type": "string", "description": "Cancellation reason captured in `SubscriptionCancelled` /\n`CancellationInitiated` events. Mirrors the values surfaced in the\nin-app cancel flow (Phase 5).", "enum": [ "too_expensive", "missing_features", "switched_service", "unused", "customer_service", "low_quality", "too_complex", "other" ] }, "CancelSubscriptionRequest": { "type": "object", "required": [ "reason_code" ], "properties": { "comment": { "type": [ "string", "null" ], "description": "Free-text detail the customer added to their cancellation reason." }, "reason_code": { "$ref": "#/components/schemas/CancelReason", "description": "Why the customer is cancelling, as picked from the cancel flow." }, "save_offer_redeemed": { "oneOf": [ { "$ref": "#/components/schemas/SaveOffer", "description": "Whether the customer accepted the retention discount instead of cancelling." }, { "type": "null" } ] }, "save_offer_shown": { "type": "array", "items": { "$ref": "#/components/schemas/SaveOffer" }, "description": "Whether the retention discount was offered during this flow." } } }, "CancelSubscriptionResponse": { "type": "object", "required": [ "period_end" ], "properties": { "period_end": { "type": "string", "format": "date-time", "description": "When the current paid period ends and access drops to the free tier." } } }, "CapabilityGap": { "oneOf": [ { "type": "object", "title": "ParsesCredentialType", "description": "The daemon cannot parse this credential type, so none of it was sent.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "ParsesCredentialType" ] } } }, { "type": "object", "title": "UsesSnmpV3SecurityOptions", "description": "The credential uses SNMPv3 options the daemon cannot parse, so none of it was sent.", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/SnmpV3SecurityOptionsGap", "description": "The credential uses SNMPv3 options the daemon cannot parse, so none of it was sent." }, "type": { "type": "string", "enum": [ "UsesSnmpV3SecurityOptions" ] } } }, { "type": "object", "title": "TargetsContainerBridgeWithCredentials", "description": "The credential targets containers on Docker or Podman bridge networks of the daemon's own\nhost, which the daemon does not try credentials at. Those addresses were not sent; the\nrest of the credential was.", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/ContainerBridgeGap", "description": "The credential targets containers on Docker or Podman bridge networks of the daemon's own\nhost, which the daemon does not try credentials at. Those addresses were not sent; the\nrest of the credential was." }, "type": { "type": "string", "enum": [ "TargetsContainerBridgeWithCredentials" ] } } } ], "description": "What a daemon missed of one credential because it is too old for part of it." }, "ChangePlanPreview": { "type": "object", "required": [ "excess_hosts", "excess_sites", "excess_seats" ], "properties": { "excess_hosts": { "type": "integer", "format": "int64", "description": "Hosts over the target plan's allowance, which would be billed as overage.", "minimum": 0 }, "excess_seats": { "type": "integer", "format": "int64", "description": "Seats over the target plan's allowance.", "minimum": 0 }, "excess_sites": { "type": "integer", "format": "int64", "description": "Sites over the target plan's allowance.", "minimum": 0 } } }, "ChangePlanRequest": { "type": "object", "required": [ "plan", "rate" ], "properties": { "plan": { "$ref": "#/components/schemas/BillingPlan", "description": "Plan to move the subscription to." }, "rate": { "$ref": "#/components/schemas/BillingRate", "description": "Billing interval to move to." } } }, "CheckEmailRequest": { "type": "object", "description": "Check email availability request", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "description": "Email address to check for an existing account." } } }, "CheckEmailResponse": { "type": "object", "description": "Check email availability response.\n\nAn address that is already registered is an answer to this question, not a\nfailure of it: the caller asked whether the address is free and got a\ntruthful no. Returning an error for that case forced every client to either\ntreat a 409 as success or suppress the toast that came with it.", "required": [ "available" ], "properties": { "available": { "type": "boolean", "description": "Whether the address has no account yet, and so can be registered." } } }, "ClaimSource": { "type": "string", "description": "Where a device's claim about itself came from.\n\nNamed rather than folded into a sentence because the operator's next step depends on it: a\nwrong `ifNumber` is a firmware bug to report upstream, while a set bridge bit over an empty\nbridge table is usually a missing SNMP view or VLAN context on their side.", "enum": [ "IfNumber", "SysServicesBridgeBit", "LldpLocalIdentity", "Dot1dBaseNumPorts" ] }, "ClientProbe": { "type": "string", "description": "Types of client probes that run before service matching.\nThe probe result (success/failure) is pre-computed; Pattern::ClientResponse\njust checks whether it succeeded.\n\nTwo types of producer fill this: the credentialed `DiscoveryIntegration`s, and the\nnon-credentialed [`AppProbe`](crate::daemon::utils::app_probe::AppProbe)s. Every variant needs\none — a variant nothing produces gives a service definition a pattern that can never match, and\n`every_client_probe_variant_has_a_producer` is what now says so.", "enum": [ "Docker", "Gnmi", "Podman", "Snmp", "UnifiController", "InstantOn", "Proxmox", "ModbusTcp", "OpcUa", "EtherNetIp", "Sip", "Ssh", "Ftp", "Telnet", "Rtsp", "Nut", "EpicsCa", "ZabbixAgent", "CheckMkAgent", "Smb", "Ldap", "Kerberos", "MySql", "PostgreSql", "MsSql", "MongoDb", "Redis", "Cassandra", "Kafka", "Amqp", "Mqtt", "OracleTns", "Rdp", "Nfs", "DnsTcp", "DockerSwarm", "Tls", "Ike", "OpenVpn", "Zmtp", "Bacula", "BeszelAgent", "H323", "SnmpEngine", "DockerPing", "PodmanPing" ] }, "ClockFormat": { "type": "string", "enum": [ "browser_default", "twelve_hour", "twenty_four_hour" ] }, "Color": { "type": "string", "enum": [ "Pink", "Rose", "Red", "Amber", "Orange", "Green", "Emerald", "Teal", "Cyan", "Blue", "Indigo", "Purple", "Fuchsia", "Violet", "Sky", "Gray", "Lime", "Yellow" ] }, "ContainerBridgeGap": { "type": "object", "description": "A credential's addresses on bridge networks of the daemon's own host.", "required": [ "addresses" ], "properties": { "addresses": { "type": "array", "items": { "type": "string" }, "description": "The credential's target addresses on bridge networks of the daemon's own host." } } }, "ContainerHostVirtualization": { "type": "object", "description": "A container with its own identity on the LAN: a macvlan or ipvlan endpoint gives it a MAC and\nan IP of its own, so it is a host under its runtime rather than a service on the runtime's\nhost. Containers on bridge networks stay services (`ServiceVirtualization`).", "required": [ "network_type" ], "properties": { "compose_project": { "type": [ "string", "null" ], "description": "Compose project the container belongs to, when it was started by Compose." }, "container_id": { "type": [ "string", "null" ], "description": "Container ID as reported by the runtime." }, "container_name": { "type": [ "string", "null" ], "description": "Container name as reported by the runtime." }, "network_type": { "$ref": "#/components/schemas/ContainerNetworkType", "description": "The network driver that gives the container its own LAN address." }, "state": { "$ref": "#/components/schemas/GuestState", "description": "Whether the container was running at the last scan." } } }, "ContainerNetworkType": { "type": "string", "description": "The container network drivers that put a container directly on the LAN.", "enum": [ "MacVlan", "IpVlan" ] }, "ContainerType": { "type": "string", "enum": [ "Subnet", "ServiceCategory", "Application", "ApplicationUngrouped", "Root", "Host", "NestedTag", "NestedServiceCategory", "Hypervisor", "ContainerRuntime", "Stack", "TrunkPort", "VLAN", "PortOpStatus" ] }, "CoveredPorts": { "type": "object", "description": "The ports of one protocol a scan covered at one address.", "required": [ "ip_address", "protocol", "ranges" ], "properties": { "ip_address": { "type": "string", "description": "The address the ports were probed at.", "example": "192.168.1.10" }, "protocol": { "$ref": "#/components/schemas/TransportProtocol", "description": "The transport the probes used." }, "ranges": { "type": "array", "items": { "$ref": "#/components/schemas/PortRange" }, "description": "The covered ports, as inclusive runs." } } }, "CreateBindingInput": { "oneOf": [ { "type": "object", "title": "IPAddress", "description": "Bind to an interface (service listens on all ports on this ip_address)", "required": [ "ip_address_id", "type" ], "properties": { "ip_address_id": { "type": "string", "format": "uuid", "description": "The IP address the service is present at." }, "type": { "type": "string", "enum": [ "IPAddress" ] } } }, { "type": "object", "title": "Port", "description": "Bind to a port (optionally on a specific ip_address)", "required": [ "port_id", "type" ], "properties": { "ip_address_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The IP address this port binding applies to. `null` binds to every IP address on the host." }, "port_id": { "type": "string", "format": "uuid", "description": "The port the service listens on." }, "type": { "type": "string", "enum": [ "Port" ] } } } ], "description": "Input for creating a binding with a service.\n`service_id` and `site_id` are assigned by the server after the service is created." }, "CreateCheckoutRequest": { "type": "object", "required": [ "plan", "url" ], "properties": { "plan": { "$ref": "#/components/schemas/BillingPlan", "description": "Plan to subscribe to." }, "url": { "type": "string", "description": "URL to return the user to after checkout completes." } } }, "CreateHostRequest": { "type": "object", "description": "Request type for creating a host with its associated ip_addresses, ports, and services.\nServer assigns `host_id`, `site_id`, and `source` to all children.\nClient must provide UUIDs for all entities, enabling services to reference\nip_addresses/ports by ID in the same request.", "required": [ "name", "site_id", "tags" ], "properties": { "chassis_id": { "type": [ "string", "null" ], "description": "LLDP chassis identifier, used to match the host to its neighbours." }, "credential_assignments": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialAssignment" }, "description": "Credentials to scan this host with." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the host." }, "hostname": { "type": [ "string", "null" ], "description": "Hostname as resolved or reported by the host." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/InterfaceInput" }, "description": "SNMP interface entries (ifTable data) - server assigns UUIDs" }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddressInput" }, "description": "Interfaces to create with this host (client provides UUIDs)" }, "management_url": { "type": [ "string", "null" ], "description": "Link to the host's own management interface." }, "name": { "type": "string", "description": "Human-facing name for the host." }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/PortInput" }, "description": "Ports to create with this host (client provides UUIDs)" }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/ServiceInput" }, "description": "Services to create with this host (can reference ip_addresses/ports by their UUIDs)" }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "sys_contact": { "type": [ "string", "null" ], "description": "SNMP sysContact — administrative contact as configured on the device." }, "sys_descr": { "type": [ "string", "null" ], "description": "SNMP sysDescr — the device's own description of itself." }, "sys_location": { "type": [ "string", "null" ], "description": "SNMP sysLocation — physical location as configured on the device." }, "sys_object_id": { "type": [ "string", "null" ], "description": "SNMP sysObjectID — the vendor's identifier for the device model." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "virtualization_interface_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The interface on the virtualizing host that presents this host, for a network identity." }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." }, { "type": "null" } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } }, "example": { "credential_assignments": [], "description": "Primary web server", "hostname": "web-server-01.local", "interfaces": [], "ip_addresses": [ { "id": "550e8400-e29b-41d4-a716-446655440005", "ip_address": "192.168.1.100", "mac_address": "DE:AD:BE:EF:12:34", "name": "eth0", "position": 0, "subnet_id": "550e8400-e29b-41d4-a716-446655440004" } ], "name": "web-server-01", "ports": [ { "id": "550e8400-e29b-41d4-a716-446655440006", "number": 80, "protocol": "Tcp" } ], "services": [ { "bindings": [ { "id": "550e8400-e29b-41d4-a716-446655440009", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "port_id": "550e8400-e29b-41d4-a716-446655440006", "type": "Port" } ], "id": "550e8400-e29b-41d4-a716-446655440007", "name": "nginx", "position": 0, "service_definition": "Consul", "tags": [], "virtualization_metadata": null, "virtualization_service_id": null } ], "site_id": "550e8400-e29b-41d4-a716-446655440002", "tags": [], "virtualization_interface_id": null, "virtualization_metadata": null, "virtualization_service_id": null } }, "CreateInviteRequest": { "type": "object", "required": [ "permissions", "site_ids" ], "properties": { "expiration_hours": { "type": [ "integer", "null" ], "format": "int64", "description": "How long the invite stays valid, in hours." }, "permissions": { "$ref": "#/components/schemas/UserOrgPermissions", "description": "Role the invited user gets on acceptance." }, "send_to": { "type": [ "string", "null" ], "description": "Address to email the invite to. Omit to create a link without sending." }, "site_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The sites this entity applies to." } } }, "CreateLicenseKeyRequest": { "type": "object", "description": "The license key to mint.", "required": [ "key_type" ], "properties": { "key_type": { "$ref": "#/components/schemas/LicenseKeyType" } } }, "CreateServiceRequest": { "type": "object", "description": "Request type for creating a service.\nServer assigns `id`, `created_at`, `updated_at`, and `source`.\nServer also assigns `service_id` and `site_id` to all bindings.", "required": [ "host_id", "site_id", "service_definition", "name", "tags" ], "properties": { "bindings": { "type": "array", "items": { "$ref": "#/components/schemas/CreateBindingInput" }, "description": "Bindings to create with the service.\n`service_id` and `site_id` are assigned by the server." }, "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "name": { "type": "string", "description": "Human-facing name for the service." }, "service_definition": { "type": "string", "description": "Which known software this service is, if identified." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/ServiceVirtualization", "description": "Container identity (name, id, compose project), when it is containerized." }, { "type": "null" } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The container runtime service hosting this container, if any." } } }, "CreateSnapshotRequest": { "type": "object", "required": [ "site_id" ], "properties": { "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." } } }, "CreateUpdateShareRequest": { "type": "object", "required": [ "share" ], "properties": { "share": { "$ref": "#/components/schemas/Share", "description": "The share to create or replace." } } }, "Credential": { "allOf": [ { "$ref": "#/components/schemas/CredentialBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "is_default" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "is_default": { "type": "boolean", "description": "Whether this is the organization's seeded default for its type: identical to the\ncredential every organization starts with. A default can't be deleted, and its settings\ncan't change. Computed from `credential_type`, so a value sent in a request is ignored.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "CredentialAssignment": { "type": "object", "description": "A credential assigned to a host, optionally limited to specific ip_addresses.", "required": [ "credential_id", "ip_address_ids" ], "properties": { "credential_id": { "type": "string", "format": "uuid", "description": "The credential this entity refers to." }, "ip_address_ids": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" }, "description": "Interface IDs to limit this credential to. None = all host ip_addresses." } } }, "CredentialAttempt": { "type": "object", "description": "One credential's attempt against one address, and what the client library said about it.", "required": [ "address", "integration", "credential_id", "detail" ], "properties": { "address": { "type": "string", "description": "The address the credential was tried against." }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential this attempt used, so the report can open it rather than sending the\nreader to the list to find it. `integration` narrows to a type; on a network with two SNMP\ncommunities the address does not narrow to a record, which is what this is for.\n\n`None` on a warning written before the id was carried, on one from a daemon that predates\nit, and on the `public` fallback older daemons injected themselves, which has no stored\nrow." }, "detail": { "type": [ "string", "null" ], "description": "The library's own diagnostic — free text, so it can only ever be displayed. It is the one\nthing the code cannot supersede: the code says which failure mode, this says what actually\ncame back (\"connection refused (os error 111)\"), and it is now attributable to this one\naddress rather than being the first message of a whole batch." }, "integration": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants" } } }, "CredentialBase": { "type": "object", "required": [ "organization_id", "name", "description", "credential_type", "tags", "assigned_site_ids", "host_assignments" ], "properties": { "assigned_site_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Sites this credential is assigned to (Broadcast scope).\nHydrated from the `site_credentials` junction table." }, "credential_type": { "$ref": "#/components/schemas/CredentialType", "description": "Protocol this credential authenticates with, and its settings." }, "daemon_os": { "oneOf": [ { "$ref": "#/components/schemas/OsFamily", "description": "The OS of the daemons that will read this credential's files and sockets. Paths are\nvalidated for it, and a daemon on another OS cannot use the credential. Set exactly when\nthe credential reads something on the daemon; `None` otherwise." }, { "type": "null" } ] }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the credential: what it is for, who owns it." }, "host_assignments": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialHostAssignment" }, "description": "Hosts this credential is assigned to (PerHost scope), with optional IP scoping.\nHydrated from the `host_credentials` junction table." }, "name": { "type": "string", "description": "Human-facing name for this credential." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." } } }, "CredentialHostAssignment": { "type": "object", "description": "Host-keyed mirror of [`CredentialAssignment`]: a host this credential is\nassigned to, optionally limited to specific ip_addresses. Hydrated onto a\ncredential from the `host_credentials` junction (PerHost scope).", "required": [ "host_id", "ip_address_ids" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "ip_address_ids": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" }, "description": "IP address IDs to limit this credential to on the host. None = all host ip_addresses." } } }, "CredentialIntegration": { "type": "string", "description": "The integration a credential type connects to: one per associated service, shared by all of\nthat service's transports (SNMP v1/v2c/v3 are three transports of the `Snmp` integration).\n\nThe single source of the type → integration mapping\n([`CredentialTypeDiscriminants::integration`]); the associated service, category, fallback\nicon, discovery text and docs guide all derive from it. The id is an identifier-safe code name\nrather than the service's display name, so it can key `meta_credential_integrations_*`\nmessages.", "enum": [ "Snmp", "Gnmi", "Docker", "Podman", "UnifiController", "InstantOn", "Ssh", "WakeOnLan", "Proxmox" ] }, "CredentialOrderField": { "type": "string", "enum": [ "created_at", "name", "updated_at", "credential_type", "integration", "beta", "unofficial_api", "daemon_os" ] }, "CredentialOsFields": { "type": "object", "description": "Which fields depend on the credential's Daemon OS. Computed from the field definitions, so the\nform places the Daemon OS picker without its own rule: with the one field that depends on it, or\nabove the first of several. The Scanned Host OS is an ordinary field of the SSH types and sits\nin its declared place, above Script, because it sets how the script runs as well as its path.", "required": [ "daemon" ], "properties": { "daemon": { "type": "array", "items": { "type": "string" }, "description": "Fields that can read a file or socket on the daemon, so depend on the credential's\n`daemon_os`: every file-or-value field, a script that can come from the daemon, and any\nfield whose placeholder follows `daemon_os`." } } }, "CredentialQueryPayloadDiscriminants": { "type": "string", "enum": [ "Snmp", "DockerProxy", "DockerSocket", "PodmanProxy", "PodmanSocket", "UnifiController", "InstantOn", "Gnmi", "Ssh", "WakeOnLan", "Proxmox", "Unknown" ] }, "CredentialRunOutcome": { "oneOf": [ { "type": "object", "title": "Collected", "description": "An integration that collects from a host: how many hosts it collected from.", "required": [ "hosts", "type" ], "properties": { "hosts": { "type": "integer", "format": "int32", "description": "Number of hosts it collected from.", "minimum": 0 }, "hosts_with_guests_listed": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The hosts whose guests (containers, VMs, network identities) it listed in full,\nstopped ones included. The server removes a guest of one of these hosts that the run\ndid not report. Daemons predating this field send none, which removes nothing." }, "type": { "type": "string", "enum": [ "Collected" ] } } }, { "type": "object", "title": "SshScript", "description": "One entry per host the script ran on.", "required": [ "runs", "type" ], "properties": { "runs": { "type": "array", "items": { "$ref": "#/components/schemas/SshScriptRun" }, "description": "The script runs, one per host." }, "type": { "type": "string", "enum": [ "SshScript" ] } } }, { "type": "object", "title": "WakeOnLan", "description": "One entry per address the daemon tried to wake.", "required": [ "hosts", "type" ], "properties": { "hosts": { "type": "array", "items": { "$ref": "#/components/schemas/WakeOnLanResult" }, "description": "The addresses woken or tried, one per address." }, "type": { "type": "string", "enum": [ "WakeOnLan" ] } } }, { "type": "object", "title": "Unknown", "description": "An outcome from a newer daemon than this server.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Unknown" ] } } } ] }, "CredentialRunResult": { "type": "object", "required": [ "credential_id", "outcome" ], "properties": { "credential_id": { "type": "string", "format": "uuid", "description": "The credential these results belong to." }, "outcome": { "$ref": "#/components/schemas/CredentialRunOutcome", "description": "What it did in this run." } } }, "CredentialStability": { "type": "string", "description": "Release maturity of a credential type's integration.\n\nAdditive and exhaustive: a new credential variant will not compile until it declares its\nstability, and every existing type is `Stable` by explicit arm rather than by wildcard, so\npromoting an integration is a one-line reviewable change rather than a deletion nobody\nnotices. This is presentation metadata about the *code*, like `last_unsupported_daemon_version` —\nit is never stored on a credential row, so it carries no deploy-coexistence obligation.", "enum": [ "Stable", "Beta" ] }, "CredentialType": { "oneOf": [ { "type": "object", "title": "SnmpV1", "description": "SNMPv1 community string — for legacy devices that only speak v1.", "required": [ "community", "type" ], "properties": { "community": { "$ref": "#/components/schemas/SecretValue", "description": "SNMPv1 community string." }, "type": { "type": "string", "enum": [ "SnmpV1" ] } } }, { "type": "object", "title": "SnmpV2c", "description": "SNMPv2c community string for querying network devices", "required": [ "community", "type" ], "properties": { "community": { "$ref": "#/components/schemas/SecretValue", "description": "SNMPv2c community string." }, "type": { "type": "string", "enum": [ "SnmpV2c" ] } } }, { "allOf": [ { "$ref": "#/components/schemas/SnmpV3Security_SecretValue", "description": "Security level and the authentication / privacy material it uses." }, { "type": "object", "required": [ "security_name" ], "properties": { "context_name": { "type": [ "string", "null" ], "description": "Optional context name (default/empty context used if unset)." }, "security_name": { "type": "string", "description": "USM security (user) name." } } }, { "type": "object", "title": "SnmpV3", "description": "SNMPv3 USM — security name, plus the authentication and privacy material its security\nlevel uses.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "SnmpV3" ] } } } ], "description": "SNMPv3 USM — security name, plus the authentication and privacy material its security\nlevel uses." }, { "type": "object", "title": "Gnmi", "description": "gNMI (gRPC Network Management Interface) credentials, for OpenConfig-speaking network\ndevices. Target IP determined from host ip_addresses at scan time, like SNMP.", "required": [ "username", "password", "type" ], "properties": { "password": { "$ref": "#/components/schemas/SecretValue", "description": "Password, sent as gRPC `password` metadata." }, "port": { "type": "integer", "format": "int32", "description": "gNMI port. 9339 is IANA's; some NOSes listen on 6030 or 57400 instead.", "minimum": 0 }, "skip_verify": { "type": "boolean", "description": "Accept any server certificate. Only meaningful with `tls` — NOS gRPC endpoints\ncommonly ship self-signed certs." }, "tls": { "type": "boolean", "description": "Use TLS. Off means plaintext gRPC (h2c) — the out-of-the-box mode of several NOSes\n(ArcOS among them: its server stays plaintext until transport-security is enabled)." }, "type": { "type": "string", "enum": [ "Gnmi" ] }, "username": { "type": "string", "description": "Username, sent as gRPC `username` metadata (the openconfig convention)." } } }, { "type": "object", "title": "DockerProxy", "description": "Docker API proxy credentials. Target IP determined from host ip_addresses at scan time.", "required": [ "type" ], "properties": { "path": { "type": [ "string", "null" ], "description": "Optional URL path prefix (e.g. \"/v1.43\")" }, "port": { "type": "integer", "format": "int32", "description": "Port for the Docker API proxy (default 2375)", "minimum": 0 }, "ssl_cert": { "oneOf": [ { "$ref": "#/components/schemas/FileOrInline", "description": "PEM-encoded public certificate — inline or file path on daemon host" }, { "type": "null" } ] }, "ssl_chain": { "oneOf": [ { "$ref": "#/components/schemas/FileOrInline", "description": "PEM-encoded CA chain — inline or file path on daemon host" }, { "type": "null" } ] }, "ssl_key": { "oneOf": [ { "$ref": "#/components/schemas/SecretValue", "description": "Private key — inline PEM content or file path on daemon host" }, { "type": "null" } ] }, "type": { "type": "string", "enum": [ "DockerProxy" ] } } }, { "type": "object", "title": "DockerSocket", "description": "Local Docker socket access on the daemon host. `socket_path` optionally repoints the\nsocket (non-default `DOCKER_HOST`); blank ⇒ the daemon auto-detects (bollard defaults).", "required": [ "type" ], "properties": { "socket_path": { "oneOf": [ { "$ref": "#/components/schemas/DaemonSocket", "description": "Path to the Docker socket. Blank lets the daemon auto-detect it." }, { "type": "null" } ] }, "type": { "type": "string", "enum": [ "DockerSocket" ] } } }, { "type": "object", "title": "PodmanProxy", "description": "Podman API proxy credentials. Podman exposes a Docker-compatible REST API,\nso the fields mirror `DockerProxy`. Target IP determined from host\nip_addresses at scan time.", "required": [ "type" ], "properties": { "path": { "type": [ "string", "null" ], "description": "Optional URL path prefix (e.g. \"/v1.43\")" }, "port": { "type": "integer", "format": "int32", "description": "Port for the Podman API proxy (default 2375)", "minimum": 0 }, "ssl_cert": { "oneOf": [ { "$ref": "#/components/schemas/FileOrInline", "description": "PEM-encoded public certificate — inline or file path on daemon host" }, { "type": "null" } ] }, "ssl_chain": { "oneOf": [ { "$ref": "#/components/schemas/FileOrInline", "description": "PEM-encoded CA chain — inline or file path on daemon host" }, { "type": "null" } ] }, "ssl_key": { "oneOf": [ { "$ref": "#/components/schemas/SecretValue", "description": "Private key — inline PEM content or file path on daemon host" }, { "type": "null" } ] }, "type": { "type": "string", "enum": [ "PodmanProxy" ] } } }, { "type": "object", "title": "PodmanSocket", "description": "Local Podman socket access on the daemon host. `socket_path` optionally repoints the\nsocket (e.g. rootful `/run/podman/podman.sock` vs rootless\n`$XDG_RUNTIME_DIR/podman/podman.sock`); blank ⇒ the daemon auto-detects via\n`resolve_podman_socket_path()`.", "required": [ "type" ], "properties": { "socket_path": { "oneOf": [ { "$ref": "#/components/schemas/DaemonSocket", "description": "Path to the Podman socket. Blank lets the daemon auto-detect it." }, { "type": "null" } ] }, "type": { "type": "string", "enum": [ "PodmanSocket" ] } } }, { "type": "object", "title": "UnifiApiKey", "description": "UniFi Network Application (controller) via an API key.\n\n**UniFi OS only** — a UniFi OS console (443) or UniFi OS Server (11443). The legacy\nself-hosted Network Application on 8443 does not support API keys; use\n[`CredentialType::UnifiLocalAdmin`] there.", "required": [ "api_key", "type" ], "properties": { "api_key": { "$ref": "#/components/schemas/SecretValue", "description": "Network Application API key, sent as `X-API-KEY`." }, "port": { "type": "integer", "format": "int32", "description": "Controller HTTPS port. 443 for a UniFi OS console, 11443 for UniFi OS Server.", "minimum": 0 }, "site": { "type": "string", "description": "Internal site name from the controller URL (`/manage/site/`)." }, "type": { "type": "string", "enum": [ "UnifiApiKey" ] } } }, { "type": "object", "title": "UnifiLocalAdmin", "description": "UniFi Network Application (controller) via a local-admin account.\n\nWorks on every controller type, including the legacy self-hosted Network Application on\n8443. Use a local-only admin account so MFA does not block the login.", "required": [ "username", "password", "type" ], "properties": { "password": { "$ref": "#/components/schemas/SecretValue", "description": "Password for that account." }, "port": { "type": "integer", "format": "int32", "description": "Controller HTTPS port. 443 UniFi OS console, 11443 UniFi OS Server, 8443 legacy.", "minimum": 0 }, "site": { "type": "string", "description": "Internal site name from the controller URL (`/manage/site/`)." }, "type": { "type": "string", "enum": [ "UnifiLocalAdmin" ] }, "username": { "type": "string", "description": "Local admin account on the controller." } } }, { "type": "object", "title": "InstantOnAccount", "description": "HPE Networking Instant On cloud portal account.\n\nThe endpoint is HPE's cloud, not a host on the network — bind this to the Instant On\nswitch it reports on. Requires an account with **MFA disabled**; use a dedicated\nsite account with the read-only Viewer role.", "required": [ "username", "password", "type" ], "properties": { "password": { "$ref": "#/components/schemas/SecretValue", "description": "Password for that account." }, "site": { "type": [ "string", "null" ], "description": "Restrict the fetch to one site by name. Blank ⇒ every site the account can see." }, "type": { "type": "string", "enum": [ "InstantOnAccount" ] }, "username": { "type": "string", "description": "Portal account email address." } } }, { "type": "object", "title": "SshPassword", "description": "SSH with a password. The daemon runs `script` on the host; the script's stdout is one JSON\nobject whose keys (`SshScriptField`) fill existing host and interface fields.", "required": [ "username", "password", "script", "type" ], "properties": { "host_key_fingerprint": { "type": [ "string", "null" ], "description": "Require this host key (OpenSSH `SHA256:…` fingerprint). Blank ⇒ the daemon pins the\nfirst key it sees per address and port, and refuses a changed one." }, "password": { "$ref": "#/components/schemas/SecretValue" }, "port": { "type": "integer", "format": "int32", "description": "Port `sshd` listens on (default 22).", "minimum": 0 }, "script": { "$ref": "#/components/schemas/ScriptSource" }, "target_os": { "$ref": "#/components/schemas/OsFamily", "description": "The scanned host's OS: decides how a host-file path is validated and the shell the\nscript runs in." }, "timeout_seconds": { "type": "integer", "format": "int32", "description": "How long the script may run before the daemon closes the channel, in seconds (default 60).", "minimum": 0 }, "type": { "type": "string", "enum": [ "SshPassword" ] }, "username": { "type": "string", "description": "User the daemon logs in as." } } }, { "type": "object", "title": "SshKey", "description": "SSH with a private key (optionally passphrase-protected). Same script contract as\n[`CredentialType::SshPassword`].", "required": [ "username", "private_key", "script", "type" ], "properties": { "host_key_fingerprint": { "type": [ "string", "null" ], "description": "Require this host key (OpenSSH `SHA256:…` fingerprint). Blank ⇒ trust on first use." }, "passphrase": { "oneOf": [ { "$ref": "#/components/schemas/SecretValue" }, { "type": "null" } ] }, "port": { "type": "integer", "format": "int32", "description": "Port `sshd` listens on (default 22).", "minimum": 0 }, "private_key": { "$ref": "#/components/schemas/SecretValue" }, "script": { "$ref": "#/components/schemas/ScriptSource" }, "target_os": { "$ref": "#/components/schemas/OsFamily", "description": "The scanned host's OS: decides how a host-file path is validated and the shell the\nscript runs in." }, "timeout_seconds": { "type": "integer", "format": "int32", "description": "How long the script may run before the daemon closes the channel, in seconds (default 60).", "minimum": 0 }, "type": { "type": "string", "enum": [ "SshKey" ] }, "username": { "type": "string", "description": "User the daemon logs in as." } } }, { "type": "object", "title": "WakeOnLan", "description": "Wake-on-LAN: the daemon wakes the assigned hosts by MAC before the sweep, so a host that\nsleeps between scans is awake to be discovered.", "required": [ "type" ], "properties": { "broadcast_address": { "type": [ "string", "null" ], "description": "Send here instead of the target subnet's directed broadcast (router relay address, WoL\nrelay device, or `255.255.255.255`).", "example": "192.168.1.1" }, "port": { "type": "integer", "format": "int32", "description": "UDP port for the magic packet.", "minimum": 0 }, "secure_on_password": { "oneOf": [ { "$ref": "#/components/schemas/SecretValue", "description": "SecureOn password, six bytes written as a MAC address. Only for NICs that require one." }, { "type": "null" } ] }, "type": { "type": "string", "enum": [ "WakeOnLan" ] }, "wait_seconds": { "type": "integer", "format": "int32", "description": "How long to wait for the woken hosts to answer before the sweep starts.", "minimum": 0 } } }, { "type": "object", "title": "ProxmoxApiToken", "description": "Proxmox VE API token. Any node's API answers for its whole cluster, so one credential on\none node reports every node, VM and LXC container. Use a read-only token: the PVEAuditor\nrole on `/`, plus guest-agent read access for VM addresses.", "required": [ "token_id", "token_secret", "type" ], "properties": { "port": { "type": "integer", "format": "int32", "description": "Proxmox VE API port (default 8006).", "minimum": 0 }, "token_id": { "type": "string", "description": "Token ID in `user@realm!tokenname` form, e.g. `scanopy@pve!discovery`." }, "token_secret": { "$ref": "#/components/schemas/SecretValue", "description": "The token's secret (a UUID), shown once when the token is created." }, "type": { "type": "string", "enum": [ "ProxmoxApiToken" ] } } } ], "description": "Universal credential type — tagged enum stored as JSONB.\nEach variant represents a different credential protocol/method." }, "CredentialTypeDiscriminants": { "type": "string", "enum": [ "SnmpV1", "SnmpV2c", "SnmpV3", "Gnmi", "DockerProxy", "DockerSocket", "PodmanProxy", "PodmanSocket", "UnifiApiKey", "UnifiLocalAdmin", "InstantOnAccount", "SshPassword", "SshKey", "WakeOnLan", "ProxmoxApiToken" ] }, "Daemon": { "allOf": [ { "$ref": "#/components/schemas/DaemonBase" }, { "type": "object", "required": [ "id", "updated_at", "created_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "DaemonApiKey": { "allOf": [ { "$ref": "#/components/schemas/DaemonApiKeyBase" }, { "type": "object", "required": [ "id", "updated_at", "created_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "DaemonApiKeyBase": { "type": "object", "required": [ "key", "name", "last_used", "site_id", "tags" ], "properties": { "daemon_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Daemon this key is bound to 1:1, when provisioned server-side.\nNULL for legacy site-shared keys created before 1:1 provisioning,\nwhich resolve daemon identity from the X-Daemon-ID header instead.", "readOnly": true }, "expires_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When this record stops being valid." }, "is_enabled": { "type": "boolean", "description": "Whether the key may still be used. Disabled keys are rejected." }, "key": { "type": "string", "description": "The stored key. Returned redacted except on creation and rotation.", "readOnly": true }, "last_used": { "type": [ "string", "null" ], "format": "date-time", "description": "When a daemon last authenticated with this key.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for this key." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." } } }, "DaemonApiKeyOrderField": { "type": "string", "description": "Fields the daemon API key list can be ordered and grouped by.", "enum": [ "created_at", "name", "updated_at", "site_id", "is_enabled", "last_used", "expires_at" ] }, "DaemonApiKeyResponse": { "type": "object", "required": [ "api_key", "key" ], "properties": { "api_key": { "$ref": "#/components/schemas/DaemonApiKey", "description": "The stored key record." }, "key": { "type": "string", "format": "password", "description": "The plaintext API key - only returned once during creation or rotation.", "readOnly": true } } }, "DaemonBase": { "type": "object", "required": [ "host_id", "site_id", "url", "mode", "name", "tags", "user_id", "os" ], "properties": { "api_key_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Foreign key to API key used for ServerPoll authentication.\nNULL for DaemonPoll daemons or those not yet linked to a key." }, "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "is_unreachable": { "type": "boolean", "description": "Whether the daemon is unreachable (for ServerPoll circuit breaker).\nSet to true after repeated polling failures, reset via retry-connection endpoint." }, "last_seen": { "type": [ "string", "null" ], "format": "date-time", "description": "Timestamp of last successful contact with daemon.\nNULL for provisioned ServerPoll daemons that haven't been contacted yet.", "readOnly": true }, "mode": { "$ref": "#/components/schemas/DaemonMode", "description": "How the daemon connects: it polls the server, or the server polls it." }, "name": { "type": "string", "description": "Human-facing name for this daemon." }, "os": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOs", "description": "The operating system the daemon runs on. Picked when the daemon is created, then checked\nagainst what the daemon reports on every handshake once it is new enough to report (see\n`version_status.supports_os_reporting`); a daemon on another OS is refused. Credentials\nset up for another OS's file paths cannot be used by it. `None` for a daemon created\nbefore this was recorded that has not yet reported." }, { "type": "null" } ] }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "standby": { "type": "boolean", "description": "Whether the daemon is on standby due to inactivity (no discovery in 30 days).", "readOnly": true }, "standby_cleared_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Timestamp of the most recent standby → active transition. Set by\n`process_startup` when a restarted daemon is un-standby'd, and by\nthe discovery auto-wake path. The nightly inactivity check skips\ndaemons within the grace window (see `STANDBY_GRACE_PERIOD_DAYS`)\nto prevent the \"restart → cleared → re-standby'd before discovery\nruns\" race.", "readOnly": true }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "url": { "type": "string", "format": "uri", "description": "Address the *server* dials for a ServerPoll daemon. Editable (a daemon can move);\nunused and not editable for DaemonPoll, which dials out instead.\nBase URL the server reaches this daemon on.", "example": "https://daemon.example.com:60073" }, "user_id": { "type": "string", "format": "uuid", "description": "User responsible for maintaining this daemon" }, "version": { "type": [ "string", "null" ], "description": "Daemon software version (semver format)", "example": "0.17.7", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" } } }, "DaemonHealth": { "type": "string", "description": "A daemon's status, the most severe that applies, in that order.\n\nUnsupported ranks above Unreachable: a rejected daemon can't connect, so its unreachability is a\nsymptom and the version is the actionable cause. The daemons list sorts and groups by it, and\nfilters on its value, which reads the same in every language.", "enum": [ "unsupported", "unreachable", "standby", "awaiting_connection", "deprecated", "outdated", "unknown", "healthy" ] }, "DaemonHeartbeatPayload": { "type": "object", "description": "Legacy heartbeat payload for backwards compatibility with pre-v0.14.0 daemons.\nOld daemons call POST /api/daemons/{id}/heartbeat with this payload.", "required": [ "url", "name", "mode" ], "properties": { "mode": { "$ref": "#/components/schemas/DaemonMode", "description": "How the daemon connects: it polls the server, or the server polls it." }, "name": { "type": "string", "description": "Name the daemon reports for itself." }, "url": { "type": "string", "description": "URL the daemon is reachable at, as it sees itself." } } }, "DaemonMode": { "type": "string", "description": "Daemon operating mode that determines the communication pattern.\n\n- **DaemonPoll** (formerly \"Pull\"): Daemon makes outbound connections to the server.\n The daemon registers itself and polls for work. Best for daemons behind NAT/firewall.\n\n- **ServerPoll** (formerly \"Push\"): Server makes connections to the daemon.\n Server polls daemon for status and discovery results. Best for DMZ deployments\n where daemon cannot make outbound connections.", "enum": [ "server_poll", "daemon_poll" ] }, "DaemonOrderField": { "type": "string", "description": "Fields that daemons can be ordered/grouped by.", "enum": [ "created_at", "name", "last_seen", "updated_at", "site_id", "os", "mode", "maintainer", "version", "status" ] }, "DaemonOs": { "type": "string", "description": "Operating system a daemon is installed on, as picked when it is created.", "enum": [ "linux", "macos", "windows", "freebsd" ] }, "DaemonPath": { "type": "string", "description": "A file on the daemon's own machine, read by the daemon at scan time." }, "DaemonPromptAction": { "type": "string", "description": "Which daemon-prompt CTA the user chose.", "enum": [ "dismissed", "accepted" ] }, "DaemonPromptResponseRequest": { "type": "object", "description": "Request recording the user's response to the \"Start Discovering Your Network\" prompt.", "required": [ "action" ], "properties": { "action": { "$ref": "#/components/schemas/DaemonPromptAction", "description": "What the user chose to do about the daemon prompt." } } }, "DaemonRegistrationRequest": { "type": "object", "description": "Daemon registration request from daemon to server", "required": [ "daemon_id", "site_id", "name", "mode" ], "properties": { "capabilities": { "$ref": "#/components/schemas/LegacyCapabilities", "description": "Legacy pre-0.15 interfaced-subnet channel (deserialize-only; see\n[`LegacyCapabilities`]). Repopulated by the first heartbeat, so registration\ndoes not persist it." }, "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon this entity refers to." }, "integration_targets": { "type": "array", "items": { "$ref": "#/components/schemas/IntegrationTarget" }, "description": "Per-daemon integration targeting from the init command (credentialed cred↔IP and\ncredential-less local sockets). Written to this daemon's Discovery at registration so\nit's present before the first session dispatches. Registration assumes new-daemon →\nnew-server, so there is no legacy bare-`credential_ids` field — bare-uuid env back-compat\nis handled in the daemon's env parser, never on the wire." }, "mode": { "$ref": "#/components/schemas/DaemonMode", "description": "How the daemon connects: it polls the server, or the server polls it." }, "name": { "type": "string", "description": "Name the daemon reports for itself." }, "os": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOs", "description": "The OS this daemon binary runs on. Absent from daemons up to 0.17.19." }, { "type": "null" } ] }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "url": { "type": [ "string", "null" ], "description": "URL is ignored by server - kept for backwards compat with old daemons.\nURL is only set via admin provisioning for ServerPoll daemons." }, "user_id": { "type": "string", "format": "uuid", "description": "User responsible for maintaining this daemon (from frontend install command)\nOptional for backwards compat with old daemons - defaults to nil UUID" }, "version": { "type": [ "string", "null" ], "description": "Daemon software version (optional for backwards compat with old daemons)" } } }, "DaemonRegistrationResponse": { "type": "object", "description": "Daemon registration response from server to daemon", "required": [ "daemon", "host_id" ], "properties": { "daemon": { "$ref": "#/components/schemas/Daemon", "description": "The registered daemon record." }, "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "server_capabilities": { "oneOf": [ { "$ref": "#/components/schemas/ServerCapabilities", "description": "Server capabilities (returned if daemon sends version info)" }, { "type": "null" } ] } } }, "DaemonResponse": { "allOf": [ { "$ref": "#/components/schemas/DaemonBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "version_status", "interfaced_subnet_ids", "health" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "health": { "$ref": "#/components/schemas/DaemonHealth", "description": "The one status the daemon's chip shows, the most severe that applies." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "interfaced_subnet_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Subnets this daemon has interfaces on, loaded from the\n`daemon_interfaced_subnets` junction (replaces the old\n`capabilities.interfaced_subnet_ids` JSONB field)." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "version_status": { "$ref": "#/components/schemas/DaemonVersionStatus", "description": "Computed version status including health and warnings" } } } ], "description": "Daemon response for UI including computed version status" }, "DaemonSocket": { "type": "string", "description": "A container runtime's API socket on the daemon: a Unix socket, or a Windows named pipe.\n\nHanded to bollard's `connect_with_socket`, which strips a `unix://` or `npipe://` scheme and\nopens a Unix socket or a named pipe for the OS it runs on." }, "DaemonStartupRequest": { "type": "object", "description": "Sent by daemon on startup to report version", "required": [ "daemon_version" ], "properties": { "daemon_version": { "type": "string", "description": "Daemon software version (semver format)" }, "os": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOs", "description": "The OS this daemon binary runs on. Absent from daemons up to 0.17.19." }, { "type": "null" } ] } } }, "DaemonStatus": { "type": "object", "description": "Lightweight daemon status for polling responses.", "required": [ "name", "mode" ], "properties": { "capabilities": { "$ref": "#/components/schemas/LegacyCapabilities", "description": "Backwards compat: pre-v0.15.0 daemons send capabilities instead of interfaced_subnets." }, "interfaced_subnets": { "type": "array", "items": { "$ref": "#/components/schemas/Subnet" }, "description": "Subnets detected from daemon's network ip_addresses. Server resolves these\nvia SubnetService::create (create-or-match by CIDR) to get real IDs.\nv0.15.0+ daemons populate this; pre-v0.15.0 daemons leave it empty." }, "mode": { "$ref": "#/components/schemas/DaemonMode", "description": "How the daemon connects: it polls the server, or the server polls it." }, "name": { "type": "string", "description": "Name the daemon reports for itself." }, "os": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOs", "description": "The OS this daemon binary runs on. Absent from daemons up to 0.17.19." }, { "type": "null" } ] }, "ready_for_work": { "type": "boolean", "description": "Whether the daemon can accept a new discovery session.\nBoth DaemonPoll and ServerPoll use this to avoid dispatching work to a busy daemon." }, "url": { "type": [ "string", "null" ], "description": "URL is not used by server - kept for backwards compat.\nServer never updates daemon URL from status (URL is set during provisioning)." }, "version": { "type": [ "string", "null" ], "description": "Daemon software version (semver format)" } } }, "DaemonVersionStatus": { "type": "object", "description": "Daemon version status including health and any warnings", "required": [ "status" ], "properties": { "persists_docker_config": { "type": "boolean", "description": "Whether a Docker daemon at this version keeps its config on the `daemon-config` volume.\nWhen false, the upgrade modal has the user copy the config onto the volume first." }, "status": { "$ref": "#/components/schemas/VersionHealthStatus", "description": "Whether that version is current, ageing, or out of support." }, "sunset_date": { "type": [ "string", "null" ], "description": "The date this daemon's version stops being supported, if a sunset is\nscheduled for it. Surfaced top-level (not only inside `warnings`) so the\nUI can render a countdown from the same value the email uses." }, "supports_os_reporting": { "type": "boolean", "description": "Whether this daemon reports its OS on every handshake. When it does, the daemon's `os` is\nwhat it runs on; when it does not, `os` is the OS picked at creation, or absent." }, "supports_targeted_rescan": { "type": "boolean", "description": "Whether this daemon can run a single-host rescan. Server-computed so the\nfrontend never has to hardcode a version gate." }, "supports_unified_discovery": { "type": "boolean", "description": "Whether the daemon can run a combined discovery pass." }, "version": { "type": [ "string", "null" ], "description": "Version the daemon reports." }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/DeprecationWarning" }, "description": "Upgrade warnings that apply to this version." } } }, "DashboardSummary": { "type": "object", "description": "Dashboard summary response", "required": [ "sites", "daemons", "recent_discoveries", "plan_usage", "uncovered_integrations" ], "properties": { "daemons": { "type": "array", "items": { "$ref": "#/components/schemas/DaemonResponse" }, "description": "Daemons the caller can see, with their current status." }, "plan_usage": { "$ref": "#/components/schemas/PlanUsage", "description": "Current usage against the organization's plan allowances." }, "recent_discoveries": { "type": "array", "items": { "$ref": "#/components/schemas/RecentDiscovery" }, "description": "The most recent discovery runs, newest first." }, "sites": { "type": "array", "items": { "$ref": "#/components/schemas/SiteSummary" }, "description": "Per-site counts for every site the caller can see." }, "uncovered_integrations": { "type": "array", "items": { "$ref": "#/components/schemas/UncoveredIntegration" }, "description": "Integrations found on hosts with no credential of them, most hosts first." } } }, "DateOrder": { "type": "string", "enum": [ "browser_default", "iso", "day_first", "month_first" ] }, "DemoPopulateStatus": { "oneOf": [ { "type": "object", "title": "Running", "required": [ "started_at", "state" ], "properties": { "started_at": { "type": "string", "format": "date-time", "description": "When population began." }, "state": { "type": "string", "enum": [ "running" ] } } }, { "type": "object", "title": "Complete", "required": [ "finished_at", "state" ], "properties": { "finished_at": { "type": "string", "format": "date-time", "description": "When population finished." }, "state": { "type": "string", "enum": [ "complete" ] } } }, { "type": "object", "title": "Failed", "required": [ "error", "finished_at", "state" ], "properties": { "error": { "type": "string", "description": "Why population failed." }, "finished_at": { "type": "string", "format": "date-time", "description": "When it gave up." }, "state": { "type": "string", "enum": [ "failed" ] } } } ], "description": "Lifecycle of a demo-populate task. `Running` is set synchronously in the\nPOST handler (before the `202`), then flipped to a terminal variant by the\nspawned task. `Failed` carries the error string so the UI can show why." }, "Dependency": { "allOf": [ { "$ref": "#/components/schemas/DependencyBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "color": "Blue", "created_at": "2026-01-15T10:30:00Z", "dependency_type": "RequestPath", "description": "HTTP/HTTPS services dependency", "edge_style": "Bezier", "id": "550e8400-e29b-41d4-a716-446655440008", "lineage_id": null, "members": { "service_ids": [], "type": "Services" }, "name": "Web Services", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "DependencyBase": { "type": "object", "required": [ "name", "site_id", "dependency_type", "members", "color", "edge_style", "tags" ], "properties": { "color": { "$ref": "#/components/schemas/Color", "description": "Colour the dependency edge is drawn in." }, "dependency_type": { "$ref": "#/components/schemas/DependencyType", "description": "What type of relationship this dependency records." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the dependency." }, "edge_style": { "$ref": "#/components/schemas/EdgeStyle", "description": "Line style the dependency edge is drawn with." }, "members": { "$ref": "#/components/schemas/DependencyMembers", "description": "Members of this dependency: either service IDs or binding IDs." }, "name": { "type": "string", "description": "Human-facing name for this dependency." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "Will be automatically set to Manual for creation through API", "readOnly": true }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." } } }, "DependencyMemberType": { "type": "string", "description": "Whether dependencies are tracked by service or by specific binding", "enum": [ "Services", "Bindings" ] }, "DependencyMembers": { "oneOf": [ { "type": "object", "title": "Services", "description": "Application-level only. Ordered list of service IDs.", "required": [ "service_ids", "type" ], "properties": { "service_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The services in the chain, in order." }, "type": { "type": "string", "enum": [ "Services" ] } } }, { "type": "object", "title": "Bindings", "description": "Full L3 detail. Ordered list of binding IDs (one per service in the chain).", "required": [ "binding_ids", "type" ], "properties": { "binding_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The bindings in the chain, in order — one per service." }, "type": { "type": "string", "enum": [ "Bindings" ] } } } ], "description": "The members of a dependency: either service-level or binding-level.\nBindings are all-or-nothing: either every position has a binding (full L3 detail)\nor none do (Application-level only)." }, "DependencyOrderField": { "type": "string", "description": "Fields that dependencies can be ordered/grouped by.", "enum": [ "created_at", "name", "dependency_type", "updated_at", "site_id" ] }, "DependencyType": { "type": "string", "enum": [ "RequestPath", "HubAndSpoke" ] }, "DependentPlaceholder": { "type": "object", "description": "A placeholder that applies while `depends_on` holds `value`.", "required": [ "depends_on", "value", "placeholder" ], "properties": { "depends_on": { "type": "string", "description": "Id of the form field this placeholder depends on (or `daemon_os` for the credential's\nDaemon OS)." }, "placeholder": { "type": "string", "description": "Placeholder shown while it does." }, "value": { "type": "string", "description": "Value that field must hold for this placeholder to apply." } } }, "DeploymentType": { "type": "string", "enum": [ "cloud", "commercial", "community" ] }, "DeprecationSeverity": { "type": "string", "description": "Severity level for deprecation warnings", "enum": [ "Info", "Warning", "Critical", "Unknown" ] }, "DeprecationWarning": { "type": "object", "description": "Deprecation warning for daemon version", "required": [ "message", "severity" ], "properties": { "message": { "type": "string", "description": "What the operator needs to do, and by when." }, "severity": { "$ref": "#/components/schemas/DeprecationSeverity", "description": "How urgent the upgrade is." }, "sunset_date": { "type": [ "string", "null" ], "description": "Date after which this daemon version stops being supported." } } }, "DeviceType": { "type": "string", "description": "What a host is, as a device: the class its icon stands for on the map.\n\nDeclaration order is precedence, highest first. A host whose services claim several device types takes\nthe first one here, so a firewall that also serves files reads as a firewall, and `Server`,\nwhich any application implies, never hides a more specific device type.\n\n`Unidentified` means no evidence yet, not a device class outside this list: every class the\nevidence names gets its own variant.", "enum": [ "Firewall", "Router", "Switch", "AccessPoint", "Hypervisor", "Storage", "Printer", "Camera", "Phone", "Workstation", "Mobile", "Industrial", "IoT", "Server", "Unidentified" ] }, "Discovery": { "allOf": [ { "$ref": "#/components/schemas/DiscoveryBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "integration_targets" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "force_full_scan": { "type": "boolean", "description": "When true, the next scan will be a full port scan regardless of interval" }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "integration_targets": { "type": "array", "items": { "$ref": "#/components/schemas/IntegrationTarget" }, "description": "Per-daemon integration targeting: which integrations run on this daemon, and on which\nIPs. Delivered via the init command at registration and editable via the discovery\nmodal. This is the single home for cred↔IP targeting; it replaces the global\n`credential.target_ips` (race-prone, consumed once).\n\nOne-shot: a target is offered to the daemon until a scan completes successfully, then\ndropped by [`Discovery::apply_successful_scan`]. Credentials that earned a durable home\nduring the scan keep being retried from there — `host_credentials` for one that probed\nsuccessfully, `site_credentials` for a broadcast one (see\n[`Discovery::take_site_scope_credential_ids`])." }, "next_run_is_full_scan": { "type": "boolean", "description": "Whether the next run of this discovery will be a full scan of all 65,535 ports rather than\na light scan of common ports. Computed by the server on read and never stored.", "readOnly": true }, "scan_count": { "type": "integer", "format": "int32", "description": "Number of completed scans (incremented by server on session completion)", "readOnly": true, "minimum": 0 }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "DiscoveryBase": { "type": "object", "required": [ "discovery_type", "run_type", "name", "daemon_id", "site_id", "tags" ], "properties": { "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon this entity refers to." }, "discovery_type": { "$ref": "#/components/schemas/DiscoveryType", "description": "What this run scans — a subnet, a single host, a container runtime, and so on." }, "name": { "type": "string", "description": "Human-facing name for this discovery." }, "run_type": { "$ref": "#/components/schemas/RunType", "description": "Whether this run was triggered by hand or on a schedule." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." } } }, "DiscoveryHostRequest": { "type": "object", "description": "Request type for daemon discovery - accepts full entities with IDs.\nUsed internally by daemons for host creation/upsert, NOT the external API.\nThis supports the discovery workflow where daemons manage entity IDs.\n\n## Backwards compatibility (daemons < v0.16.0)\n\nPre-v0.16.0 daemons send the old field layout:\n - `interfaces` → IPAddress data (now `ip_addresses`)\n - `if_entries` → SNMP Interface data (now `interfaces`)\n\nThe custom deserializer detects the old layout (missing `ip_addresses` field)\nand remaps fields automatically. This can be removed once all daemons are ≥ v0.16.0.", "required": [ "host", "ip_addresses", "ports", "services" ], "properties": { "host": { "$ref": "#/components/schemas/Host", "description": "The host as observed by the daemon." }, "interface_data_complete": { "$ref": "#/components/schemas/InterfaceDataComplete", "description": "Which groups of per-interface data (LLDP, CDP, FDB, VLAN membership) this scan read in\nfull. A group the daemon could not finish reading must not overwrite what is already\nstored: a cut-short walk returns the same empty result as a device with nothing to report,\nand for the neighbour fields that also drops the row out of L2 resolution for good.\nDaemons predating this field omit it; it defaults to all-complete so they behave as before." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/Interface" }, "description": "SNMP interface entries (ifTable data) - optional, populated when SNMP is enabled." }, "interfaces_complete": { "type": "boolean", "description": "Whether `interfaces` is a complete, authoritative ifTable. When false (a partial SNMP walk\ncut short by timeout/error), the server must NOT prune interfaces missing from this scan —\notherwise a transient partial walk tears down the host's L2 topology (#649). Daemons that\npredate this field omit it; it defaults to true so their behavior is unchanged." }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "IP addresses observed on the host." }, "port_coverage": { "$ref": "#/components/schemas/PortCoverage", "description": "Every (address, protocol, port) this scan probed and got a definite answer from. A stored\nport binding can change hands only where the scan covered it. Daemons predating this field\nomit it, and empty coverage moves nothing." }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/Port" }, "description": "Open ports observed on the host." }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Services identified on the host." }, "subnets": { "type": "array", "items": { "$ref": "#/components/schemas/Subnet" }, "description": "Integration-derived subnets (e.g., Docker bridge networks) — created during\ncreate_with_children after service dedup so virtualization.service_id is correct." } } }, "DiscoveryOrderField": { "type": "string", "description": "Fields that discoveries can be ordered/grouped by.", "enum": [ "created_at", "name", "updated_at", "daemon_id", "site_id", "discovery_type", "phase", "started_at", "finished_at", "duration", "warnings", "run_type", "last_run", "scan_count", "force_full_scan", "legacy" ] }, "DiscoveryPhase": { "type": "string", "enum": [ "AwaitingSnapshot", "Queued", "Pending", "Starting", "Started", "Scanning", "Complete", "Failed", "Cancelled" ] }, "DiscoveryProtocol": { "type": "string", "description": "Protocol that discovered the physical link between network devices", "enum": [ "LLDP", "CDP" ] }, "DiscoveryTerminalReason": { "type": "string", "description": "Why a session reached its terminal phase.\n\n`phase` says *that* a run ended; this says *why*. A stall reaped by the server, a daemon that\nlost its state in a restart and a user pressing cancel all used to land as the same phase with\nno way to tell them apart afterwards, which is what made stall reports undiagnosable.\n\nThe server decides every reason except the two the daemon alone can know, see\n[`Self::daemon_assigned`]. Absent on runs recorded before this field existed.", "enum": [ "Completed", "UserCancelled", "DaemonReportedFailure", "StalledNoUpdates", "DaemonRestarted", "DaemonUnreachable", "DaemonBusy", "WatchdogTimeout" ] }, "DiscoveryType": { "oneOf": [ { "type": "object", "title": "SelfReport", "required": [ "host_id", "type" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "The host the daemon is running on." }, "type": { "type": "string", "enum": [ "SelfReport" ] } } }, { "type": "object", "title": "Network", "required": [ "subnet_ids", "host_naming_fallback", "type" ], "properties": { "host_naming_fallback": { "$ref": "#/components/schemas/HostNamingFallback", "description": "What to name a host by when reverse DNS gives nothing." }, "snmp_credentials": { "type": "object", "description": "SNMP credentials for querying devices during discovery\nServer builds this mapping before initiating discovery" }, "subnet_ids": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" }, "description": "Subnets to sweep. `null` sweeps every subnet on the site." }, "type": { "type": "string", "enum": [ "Network" ] } } }, { "type": "object", "title": "Docker", "required": [ "host_id", "host_naming_fallback", "type" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "The host the daemon is running on." }, "host_naming_fallback": { "$ref": "#/components/schemas/HostNamingFallback", "description": "What to name a host by when reverse DNS gives nothing." }, "type": { "type": "string", "enum": [ "Docker" ] } } }, { "type": "object", "title": "Rescan", "description": "A one-shot verification of known hosts: re-check the addresses and ports\nalready recorded for them, rather than sweeping a subnet. One rescan\ncovers every requested host that the same daemon last found.\n\nCreated by the server only (never via the API) and deleted once its\nsession reaches a terminal phase, so it is not a discovery configuration\nanyone owns or sees in their scan list.", "required": [ "host_id", "target_host_ids", "ips", "type" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "ID of the host that the daemon is running on — same meaning as every\nother variant. The hosts being rescanned are `target_host_ids`." }, "ips": { "type": "array", "items": { "type": "string" }, "description": "Addresses to scan on those hosts." }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/PortType" }, "description": "Ports already known on those hosts, re-checked on every address to\nconfirm they are still open. Scanned in addition to the standard\ndiscovery set, so a rescan also surfaces newly-opened services." }, "settings": { "$ref": "#/components/schemas/RescanSettings" }, "target_host_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The hosts being rescanned. Reads the single `target_host_id` that\nrescans stored before multi-host rescans carry, and that older\ndaemons echo back, as a list of one." }, "type": { "type": "string", "enum": [ "Rescan" ] } } }, { "type": "object", "title": "Unified", "required": [ "host_id", "subnet_ids", "host_naming_fallback", "type" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "ID of the host that the daemon is running on" }, "host_naming_fallback": { "$ref": "#/components/schemas/HostNamingFallback", "description": "Fallback strategy for naming discovered hosts" }, "scan_settings": { "$ref": "#/components/schemas/ScanSettings", "description": "Per-discovery scan performance settings" }, "subnet_ids": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" }, "description": "Subnets to scan. None = scan all interfaced subnets." }, "type": { "type": "string", "enum": [ "Unified" ] } } } ] }, "DiscoveryUpdatePayload": { "type": "object", "description": "Progress update from daemon to server during discovery", "required": [ "session_id", "daemon_id", "site_id", "phase", "discovery_type", "progress" ], "properties": { "credential_results": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialRunResult" }, "description": "What each stored credential did in this run, one entry per credential. Results, not\nproblems: those are `warnings`. Old servers ignore it.\n\nRead per entry, like `warnings`: an outcome shape from a newer daemon becomes `Unknown`\nfor that credential instead of failing the terminal payload." }, "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon this entity refers to." }, "daemon_version": { "type": [ "string", "null" ], "description": "The daemon's version when the run ended. Stamped by the server at terminal, so it\nsurvives the daemon being deleted or upgraded." }, "discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery configuration this session belongs to.\nAlways enriched server-side; daemons do not send this field." }, "discovery_type": { "$ref": "#/components/schemas/DiscoveryType", "description": "What type of discovery is running." }, "error": { "type": [ "string", "null" ], "description": "Failure message, when the run did not complete." }, "estimated_remaining_secs": { "type": [ "integer", "null" ], "format": "int32", "description": "Rough estimate of the time left, in seconds.", "minimum": 0 }, "finished_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the run finished. `null` while it is still going." }, "hosts_discovered": { "type": [ "integer", "null" ], "format": "int32", "description": "Hosts found so far.", "minimum": 0 }, "last_update_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the server last heard about this run. Stamped by the server at terminal." }, "phase": { "$ref": "#/components/schemas/DiscoveryPhase", "description": "Which stage of the run is in progress." }, "progress": { "type": "integer", "format": "int32", "description": "Completion of the current phase, from 0 to 1.", "minimum": 0 }, "reason": { "oneOf": [ { "$ref": "#/components/schemas/DiscoveryTerminalReason", "description": "Why the run ended. Set on terminal payloads; absent on runs recorded before it existed.\nA daemon may send one of the reasons only it can know; the server assigns every other.\nA reason this server doesn't know reads as `None`, and the server assigns one from the\nphase." }, { "type": "null" } ] }, "scanned": { "oneOf": [ { "$ref": "#/components/schemas/ScannedEntityIds", "description": "Canonical IDs of entities scanned in this session, populated daemon-\nside at terminal. **Transient**: stripped at `SqlValue::RunType` bind\ntime so it doesn't persist into the historical Discovery row's JSONB.\nAvailable in-memory through the `EntityOperation::Created` event\npublished for the historical Discovery row (the event scope carries\n`Entity::Discovery(...)`, the full in-memory struct), where per-entity\nFK-update subscribers consume it.\n\n`Some(...)` when the daemon is sending the terminal payload over the\nwire. `None` when read back from a persisted historical row, or when\nnot yet set." }, { "type": "null" } ] }, "session_id": { "type": "string", "format": "uuid", "description": "The discovery run this update belongs to." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "started_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the run started." }, "warnings": { "type": "array", "items": { "$ref": "#/components/schemas/DiscoveryWarning" }, "description": "Non-fatal findings from a completed run — one per occurrence, each carrying the code that\nidentifies it and the detail that fills the sentence. Unlike `error`, these do not mark the\nrun failed.\n\nRead through [`deserialize_warnings`] rather than the derived impl, which is what keeps\nhistorical records and pre-coded daemons rendering: both send bare strings here, and both\nland as `Unknown` carrying that text instead of failing the whole payload." } } }, "DiscoveryWarning": { "oneOf": [ { "type": "object", "title": "InterfaceSetCutShort", "description": "The interface *set* was cut short, so interfaces are genuinely missing.", "required": [ "address", "collected", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device whose walk fell short." }, "code": { "type": "string", "enum": [ "InterfaceSetCutShort" ] }, "collected": { "type": "integer", "format": "int32", "description": "Interfaces read before the walk stopped.", "minimum": 0 }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." } } }, { "type": "object", "title": "InterfaceDetailsCutShort", "description": "The set was complete and only the attribute columns fell short, so nothing is missing —\nsome descriptions or speeds are just blank. Kept apart from the above because reporting\nthis as possible data loss sends people hunting for interfaces that were never absent.", "required": [ "address", "collected", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device whose walk fell short." }, "code": { "type": "string", "enum": [ "InterfaceDetailsCutShort" ] }, "collected": { "type": "integer", "format": "int32", "description": "Interfaces whose attribute columns were read in full.", "minimum": 0 }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." } } }, { "type": "object", "title": "SnmpWalkEntryCap", "description": "Stopped at our own entry cap. The device is fine and larger than we read.", "required": [ "address", "group", "limit", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkEntryCap" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" }, "limit": { "type": "integer", "format": "int32", "description": "Entries per table that collection stops at.", "minimum": 0 } } }, { "type": "object", "title": "SnmpWalkUnsupported", "description": "The device does not implement this MIB. Not a fault, and no later scan will change it.", "required": [ "address", "group", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkUnsupported" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "SnmpWalkDesynchronised", "description": "The agent answered out of step with what was asked — stale or non-advancing responses.", "required": [ "address", "group", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkDesynchronised" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "SnmpWalkPartialDiscarded", "description": "A partial read whose rows are thrown away rather than recorded, so the device contributes\nnothing for this group however much it answered.", "required": [ "address", "group", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkPartialDiscarded" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "SnmpWalkPartialRecorded", "description": "A partial read whose rows were recorded as far as they got.", "required": [ "address", "group", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkPartialRecorded" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "SnmpWalkBridgeMibAbsent", "description": "Every request for the root of the bridge MIB timed out, which is how switches that do not\nimplement it commonly answer.", "required": [ "address", "group", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkBridgeMibAbsent" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "SnmpWalkNoAnswer", "description": "The read returned nothing, and its cause was not recorded: a daemon from before causes were\ncarried, or a read that ended without a failed request.", "required": [ "address", "group", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkNoAnswer" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "SnmpWalkFailed", "description": "The read returned nothing, and `cause` is how its last request ended.", "required": [ "address", "group", "cause", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "cause": { "$ref": "#/components/schemas/SnmpWalkCause" }, "code": { "type": "string", "enum": [ "SnmpWalkFailed" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "ClaimedCountReadCutShort", "description": "The device published a count, and the read ended before reaching it.", "required": [ "address", "group", "source", "expected", "observed", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device that published the count." }, "code": { "type": "string", "enum": [ "ClaimedCountReadCutShort" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "expected": { "type": "integer", "format": "int32", "description": "Rows the device said it had.", "minimum": 0 }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" }, "observed": { "type": "integer", "format": "int32", "description": "Rows the read returned.", "minimum": 0 }, "source": { "$ref": "#/components/schemas/ClaimSource" } } }, { "type": "object", "title": "ClaimedCountUnderRead", "description": "The device published a count, the read finished, and it came up short anyway.", "required": [ "address", "group", "source", "expected", "observed", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device that published the count." }, "code": { "type": "string", "enum": [ "ClaimedCountUnderRead" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "expected": { "type": "integer", "format": "int32", "description": "Rows the device said it had.", "minimum": 0 }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" }, "observed": { "type": "integer", "format": "int32", "description": "Rows the read returned.", "minimum": 0 }, "source": { "$ref": "#/components/schemas/ClaimSource" } } }, { "type": "object", "title": "ClaimedCapabilityReadCutShort", "description": "The device declared the capability, and the read ended without returning any.", "required": [ "address", "group", "source", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device that declared the capability." }, "code": { "type": "string", "enum": [ "ClaimedCapabilityReadCutShort" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" }, "source": { "$ref": "#/components/schemas/ClaimSource" } } }, { "type": "object", "title": "ClaimedCapabilityEmpty", "description": "The device declared the capability, the read finished, and it returned none.", "required": [ "address", "group", "source", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device that declared the capability." }, "code": { "type": "string", "enum": [ "ClaimedCapabilityEmpty" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" }, "source": { "$ref": "#/components/schemas/ClaimSource" } } }, { "type": "object", "title": "LldpLocalPortDropped", "description": "Neighbours whose local port matched no interface, so they were discarded entirely.", "required": [ "address", "dropped", "total", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device that reported the neighbours." }, "code": { "type": "string", "enum": [ "LldpLocalPortDropped" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "dropped": { "type": "integer", "format": "int32", "description": "Neighbours discarded for want of a matching interface.", "minimum": 0 }, "total": { "type": "integer", "format": "int32", "description": "Neighbours the device reported in all.", "minimum": 0 } } }, { "type": "object", "title": "LldpLocalPortDroppedReadCutShort", "description": "The same loss, on a device whose own account of its port numbering was only half read.\n\nSeparate from [`Self::LldpLocalPortDropped`] because that one names a cause — the switch\nnumbering its LLDP ports apart from its interfaces — which is a property of the device and\nis not what happened here. Offering both readings in one sentence, as this used to, asks\nthe operator to pick; one says nothing will ever change and the other says a complete scan\nmay well place these neighbours (GH #668).", "required": [ "address", "dropped", "total", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device that reported the neighbours." }, "code": { "type": "string", "enum": [ "LldpLocalPortDroppedReadCutShort" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "dropped": { "type": "integer", "format": "int32", "description": "Neighbours discarded for want of a matching interface.", "minimum": 0 }, "total": { "type": "integer", "format": "int32", "description": "Neighbours the device reported in all.", "minimum": 0 } } }, { "type": "object", "title": "LldpLocalPortMisplaced", "description": "Neighbours whose local port could not be identified but did match an interface number, so\nthey are drawn against a port that may be the wrong one.", "required": [ "address", "misplaced", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The device that reported the neighbours." }, "code": { "type": "string", "enum": [ "LldpLocalPortMisplaced" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "misplaced": { "type": "integer", "format": "int32", "description": "Neighbours drawn against a port that may be the wrong one.", "minimum": 0 } } }, { "allOf": [ { "$ref": "#/components/schemas/MalformedNeighbours", "description": "The column carrying the identifier stopped early, so a rescan may recover these." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "MalformedNeighboursWalkCutShort" ] } } } ], "description": "The column carrying the identifier stopped early, so a rescan may recover these." }, { "allOf": [ { "$ref": "#/components/schemas/MalformedNeighbours", "description": "Rows that never appeared in the identifying column at all." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "MalformedNeighboursGhostRows" ] } } } ], "description": "Rows that never appeared in the identifying column at all." }, { "allOf": [ { "$ref": "#/components/schemas/MalformedNeighbours", "description": "Neighbours listed and then never given an identifier." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "MalformedNeighboursIncompleteRecords" ] } } } ], "description": "Neighbours listed and then never given an identifier." }, { "allOf": [ { "$ref": "#/components/schemas/MalformedNeighbours", "description": "The identifying column held a value of a type it cannot hold." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "MalformedNeighboursUnexpectedType" ] } } } ], "description": "The identifying column held a value of a type it cannot hold." }, { "allOf": [ { "$ref": "#/components/schemas/MalformedNeighbours", "description": "The record's position in the neighbour table could not be read." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "MalformedNeighboursUnreadableIndex" ] } } } ], "description": "The record's position in the neighbour table could not be read." }, { "type": "object", "title": "SnmpCollectedNothing", "description": "SNMP answered and every table came back empty.", "required": [ "address", "credential_id", "failures", "code" ], "properties": { "address": { "type": "string", "description": "The device that answered." }, "code": { "type": "string", "enum": [ "SnmpCollectedNothing" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "failures": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/SnmpWalkFailure" }, "description": "Each table whose read failed, and how. These are not reported again on their own.\nEmpty when every read ended normally; `None` from a daemon that predates the field,\nwhich recorded only that nothing came back." } } }, { "type": "object", "title": "VlanRecordingFailed", "description": "The device answered correctly and persisting its VLANs failed.", "required": [ "address", "code" ], "properties": { "address": { "type": "string", "description": "The device whose VLANs could not be recorded." }, "code": { "type": "string", "enum": [ "VlanRecordingFailed" ] } } }, { "type": "object", "title": "EqualReachIntegrationsMerged", "description": "Two integrations that each read the device's full interface table both collected it, and\ntheir interface sets were merged.\n\nA supported configuration, not a fault. It is reported because the merge keeps one row\nper port: where both describe a port, the first to answer supplies the row and its\nneighbours, and the second's are dropped. Someone chasing a missing link needs to know\nthat happened.", "required": [ "address", "first", "second", "code" ], "properties": { "address": { "type": "string", "description": "The device both integrations read." }, "code": { "type": "string", "enum": [ "EqualReachIntegrationsMerged" ] }, "first": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants", "description": "The integration that answered first. Its row stands on every port both describe." }, "second": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants", "description": "The integration that answered second." } } }, { "type": "object", "title": "CredentialTargetNotScanned", "description": "The credential's address is not on any subnet this scan covers.", "required": [ "address", "integration", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The address the credential is bound to." }, "code": { "type": "string", "enum": [ "CredentialTargetNotScanned" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential bound to that address. See [`CredentialAttempt::credential_id`]." }, "integration": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants" } } }, { "type": "object", "title": "CredentialTargetNotResponding", "description": "Nothing answered at the credential's address during the scan.", "required": [ "address", "integration", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The address the credential is bound to." }, "code": { "type": "string", "enum": [ "CredentialTargetNotResponding" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential bound to that address. See [`CredentialAttempt::credential_id`]." }, "integration": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants" } } }, { "type": "object", "title": "CredentialDaemonOsMismatch", "description": "The credential's files or sockets were declared for one OS and this daemon runs another, so\nit was not used here.", "required": [ "integration", "credential_id", "declared", "actual", "code" ], "properties": { "actual": { "$ref": "#/components/schemas/OsFamily", "description": "The OS family this daemon runs." }, "actual_os": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOs", "description": "The OS this daemon runs, to name it. Absent on runs recorded before it was sent." }, { "type": "null" } ] }, "code": { "type": "string", "enum": [ "CredentialDaemonOsMismatch" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential. See [`CredentialAttempt::credential_id`]." }, "declared": { "$ref": "#/components/schemas/OsFamily", "description": "The OS the credential's paths were declared for." }, "integration": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants" } } }, { "type": "object", "title": "CredentialNeedsNewerDaemon", "description": "The daemon is too old for part of how this credential is configured, so the server withheld\nthat part (or, for a type the daemon cannot parse, all of it). `gap` names what. Raised by\nthe server, never a daemon: the daemon that would report it is the one that lacks it.", "required": [ "integration", "credential_id", "gap", "last_unsupported_daemon_version", "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialNeedsNewerDaemon" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential. See [`CredentialAttempt::credential_id`]." }, "daemon_version": { "type": [ "string", "null" ], "description": "The version of the daemon that ran the scan. `None` for a daemon that never reported one.", "example": "0.18.0", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" }, "gap": { "$ref": "#/components/schemas/CapabilityGap", "description": "What the daemon missed." }, "integration": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants" }, "last_unsupported_daemon_version": { "type": "string", "description": "The last daemon release without what `gap` needs; any newer daemon has it.", "example": "0.18.0", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" } } }, { "type": "object", "title": "CredentialGateClosed", "description": "The port the credential needs was not open, so it was never tried.", "required": [ "address", "integration", "ports", "credential_id", "code" ], "properties": { "address": { "type": "string", "description": "The address the credential is bound to." }, "code": { "type": "string", "enum": [ "CredentialGateClosed" ] }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential that needed those ports. See [`CredentialAttempt::credential_id`]." }, "integration": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants" }, "ports": { "type": "array", "items": { "type": "integer", "format": "int32", "minimum": 0 }, "description": "The ports that had to be open for the probe to run." } } }, { "allOf": [ { "$ref": "#/components/schemas/CredentialAttempt", "description": "The credential was refused." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialRejected" ] } } } ], "description": "The credential was refused." }, { "allOf": [ { "$ref": "#/components/schemas/CredentialAttempt", "description": "The credential is incomplete and could not be used." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialMalformed" ] } } } ], "description": "The credential is incomplete and could not be used." }, { "allOf": [ { "$ref": "#/components/schemas/CredentialAttempt", "description": "TLS could not be negotiated." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialTlsFailed" ] } } } ], "description": "TLS could not be negotiated." }, { "allOf": [ { "$ref": "#/components/schemas/CredentialAttempt", "description": "Something answered that is not the expected service." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialNotThisService" ] } } } ], "description": "Something answered that is not the expected service." }, { "allOf": [ { "$ref": "#/components/schemas/CredentialAttempt", "description": "Authenticated, then failed while collecting." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialCollectionFailed" ] } } } ], "description": "Authenticated, then failed while collecting." }, { "allOf": [ { "$ref": "#/components/schemas/CredentialAttempt", "description": "Authenticated, then ran out of time while collecting." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialCollectionTimedOut" ] } } } ], "description": "Authenticated, then ran out of time while collecting." }, { "allOf": [ { "$ref": "#/components/schemas/CredentialAttempt", "description": "Nothing was reachable at the address." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialUnreachable" ] } } } ], "description": "Nothing was reachable at the address." }, { "allOf": [ { "$ref": "#/components/schemas/CredentialAttempt", "description": "The attempt timed out before anything answered." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "CredentialTimedOut" ] } } } ], "description": "The attempt timed out before anything answered." }, { "type": "object", "title": "IntegrationWithoutCredential", "description": "The scan found a service an integration reads, on a host no credential of that integration\nis assigned to. Raised by the server at the end of a run, never a daemon: coverage is a\nfact about stored assignments, which the daemon does not hold. One per host and\nintegration; the UI groups them.", "required": [ "host_id", "integration", "code" ], "properties": { "code": { "type": "string", "enum": [ "IntegrationWithoutCredential" ] }, "host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The host running the service. `None` once the host is deleted." }, "integration": { "$ref": "#/components/schemas/CredentialIntegration", "description": "The integration that would read it." } } }, { "type": "object", "title": "ConnectionsWithoutProtocolResponse", "description": "Addresses on a routed subnet completed a TCP handshake and then answered nothing, so no\nhost was recorded at any of them.\n\nThe shape a middlebox in the path produces: a firewall session helper, a load balancer or a\nscrubbing appliance answering on behalf of addresses with nothing behind them. One warning\nper subnet rather than per address — the case that raises it raises it for every address in\nthe range, and 254 copies of a sentence is not 254 findings.", "required": [ "cidr", "declined", "ports", "code" ], "properties": { "cidr": { "type": "string", "description": "The subnet the addresses are in." }, "code": { "type": "string", "enum": [ "ConnectionsWithoutProtocolResponse" ] }, "declined": { "type": "integer", "format": "int32", "description": "How many addresses in it answered a connect and nothing else.", "minimum": 0 }, "ports": { "type": "array", "items": { "type": "integer", "format": "int32", "minimum": 0 }, "description": "The ports that completed a handshake, most frequent first." } } }, { "type": "object", "title": "ScanTimeLimitWithEstimate", "description": "The run hit its global time limit, with an estimate of the work left.", "required": [ "hours", "hosts_not_scanned", "minutes_remaining", "code" ], "properties": { "code": { "type": "string", "enum": [ "ScanTimeLimitWithEstimate" ] }, "hosts_not_scanned": { "type": "integer", "format": "int32", "description": "Hosts still queued when the run stopped.", "minimum": 0 }, "hours": { "type": "integer", "format": "int32", "description": "The limit the run hit, in hours.", "minimum": 0 }, "minutes_remaining": { "type": "integer", "format": "int32", "description": "Estimated minutes of work left at that point.", "minimum": 0 } } }, { "type": "object", "title": "ScanTimeLimit", "description": "The run hit its global time limit, with no usable estimate.", "required": [ "hours", "hosts_not_scanned", "code" ], "properties": { "code": { "type": "string", "enum": [ "ScanTimeLimit" ] }, "hosts_not_scanned": { "type": "integer", "format": "int32", "description": "Hosts still queued when the run stopped.", "minimum": 0 }, "hours": { "type": "integer", "format": "int32", "description": "The limit the run hit, in hours.", "minimum": 0 } } }, { "type": "object", "title": "DcpSweepTimedOut", "description": "The PROFINET DCP sweep did not report back within its budget, so the run finished\nwithout the devices only it would have found.", "required": [ "seconds", "code" ], "properties": { "code": { "type": "string", "enum": [ "DcpSweepTimedOut" ] }, "seconds": { "type": "integer", "format": "int32", "description": "The budget the sweep had, in seconds.", "minimum": 0 } } }, { "type": "object", "title": "IcmpSweepTimedOut", "description": "The ICMP sweep did not report back within its budget, so hosts that answer only to ping\nwere not scanned.", "required": [ "seconds", "code" ], "properties": { "code": { "type": "string", "enum": [ "IcmpSweepTimedOut" ] }, "seconds": { "type": "integer", "format": "int32", "description": "The budget the sweep had, in seconds.", "minimum": 0 } } }, { "type": "object", "title": "ReverseDnsTimedOut", "description": "Reverse DNS lookups that got no answer in time, so those hosts have no DNS hostname.", "required": [ "count", "code" ], "properties": { "code": { "type": "string", "enum": [ "ReverseDnsTimedOut" ] }, "count": { "type": "integer", "format": "int32", "description": "How many lookups went unanswered.", "minimum": 0 } } }, { "allOf": [ { "$ref": "#/components/schemas/UnmatchedNeighbour", "description": "The advertised identifier matches no host on this site." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "LldpNeighbourNotFound" ] } } } ], "description": "The advertised identifier matches no host on this site." }, { "allOf": [ { "$ref": "#/components/schemas/UnmatchedNeighbour", "description": "The advertised identifier matches several hosts, so none can be picked." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "LldpNeighbourAmbiguous" ] } } } ], "description": "The advertised identifier matches several hosts, so none can be picked." }, { "allOf": [ { "$ref": "#/components/schemas/UnresolvedPort", "description": "The far end resolved, and its port id is of a subtype there is no lookup for." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "LldpPortNoStrategy" ] } } } ], "description": "The far end resolved, and its port id is of a subtype there is no lookup for." }, { "allOf": [ { "$ref": "#/components/schemas/UnresolvedPort", "description": "The far end resolved, and none of its ports matches the advertised port id." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "LldpPortNotFound" ] } } } ], "description": "The far end resolved, and none of its ports matches the advertised port id." }, { "allOf": [ { "$ref": "#/components/schemas/UnresolvedPort", "description": "The far end resolved, and several of its ports match, so it identifies none." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "LldpPortAmbiguous" ] } } } ], "description": "The far end resolved, and several of its ports match, so it identifies none." }, { "allOf": [ { "$ref": "#/components/schemas/ProvisionalSubnet", "description": "A range was created from the addresses unplaceable far ends publish for themselves." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "ProvisionalSubnetInferred" ] } } } ], "description": "A range was created from the addresses unplaceable far ends publish for themselves." }, { "type": "object", "title": "NeighbourResolutionIncomplete", "description": "Link resolution ran out of its time budget, so this scan's links are incomplete.\n\nThe pass runs on the completion request, which the daemon will abandon at its own timeout —\nso it is bounded, and stopping has to be *said* rather than logged. Without this the\ndegraded result is indistinguishable from a network that genuinely has no neighbours.", "required": [ "budget_seconds", "neighbours", "code" ], "properties": { "budget_seconds": { "type": "integer", "format": "int32", "description": "Seconds the pass was allowed before it was stopped.", "minimum": 0 }, "code": { "type": "string", "enum": [ "NeighbourResolutionIncomplete" ] }, "neighbours": { "type": "integer", "format": "int32", "description": "Interfaces carrying neighbour data it was working through, so the reader can tell\n\"this network is large\" from \"something is wrong\".", "minimum": 0 } } }, { "type": "object", "title": "FdbResolutionIncomplete", "description": "FDB link resolution ran out of its time budget, so this scan's FDB-derived links are\nincomplete.\n\nDistinct from `NeighbourResolutionIncomplete`: that pass runs first and this one is\nstrictly sequenced after it, so a reader needs to know which pass stopped to judge how\nmuch of Physical Topology is affected.", "required": [ "budget_seconds", "interfaces", "code" ], "properties": { "budget_seconds": { "type": "integer", "format": "int32", "description": "Seconds the pass was allowed before it was stopped.", "minimum": 0 }, "code": { "type": "string", "enum": [ "FdbResolutionIncomplete" ] }, "interfaces": { "type": "integer", "format": "int32", "description": "Interfaces with an unresolved single-MAC FDB entry it was working through.", "minimum": 0 } } }, { "type": "object", "title": "OutdatedDaemonFormat", "description": "The daemon submitted this scan in a wire format a current daemon no longer produces.\n\nServer-side finding, raised where the raw request body is read. The server translates the\nold format, so nothing was lost — this says the translation happened, which is the only\nevidence an operator ever gets that a daemon is behind. Which superseded format it was is\ndeliberately not carried, and neither is the fact that the translation worked: the reader\nneeds one thing from this, which is that the daemon is out of date.\n\nThe daemon to upgrade is the session's own — the UI reads `daemon_id` off the payload this\nrides on rather than repeating it here, which is what lets the row deep-link to that\ndaemon's upgrade modal.\n\nEnds with the compatibility window. When the enforced floor rises past the last release\nthat sends an old format, the translations go and this goes with them.", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "OutdatedDaemonFormat" ] }, "daemon_version": { "type": [ "string", "null" ], "description": "The submitting daemon's version, so the reader knows which one to upgrade. `None` for\na daemon too old to report one at all.", "example": "0.17.14", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" } } }, { "type": "object", "title": "WarningsTruncated", "description": "The run produced more warnings than the scan record holds. Emitted rather than dropping\nthe tail silently — a list that simply stops reads as though that was all of them.", "required": [ "elided", "code" ], "properties": { "code": { "type": "string", "enum": [ "WarningsTruncated" ] }, "elided": { "type": "integer", "format": "int32", "description": "Warnings dropped past the record's cap.", "minimum": 0 } } }, { "type": "object", "title": "Unknown", "description": "A warning this binary does not recognise: a bare string from a historical record or a\npre-coded daemon, or a code from a newer one. Carries the original text so scan history\nkeeps rendering; the code itself is what reaches the metric, never `detail`.", "required": [ "detail", "code" ], "properties": { "code": { "type": "string", "enum": [ "Unknown" ] }, "detail": { "type": "string", "description": "The original warning text, rendered as-is." } } } ], "description": "A single non-fatal finding from one discovery run, about one device, neighbour, or the scan\nitself.\n\nSerialized with the code as the tag, so the generated TypeScript is a discriminated union the\nUI can switch on exhaustively. The derived `Deserialize` reads that shape; the leniency that\nkeeps historical records and pre-coded daemons working lives in [`deserialize_warnings`],\nwhich is applied at the one field that holds these." }, "DisplaySettings": { "type": "object", "description": "Per-user display preferences for dates and times in the UI.\n\nStored as a JSONB blob with `#[serde(default)]` on the struct, so a stored\nobject missing a key (including the `{}` every existing row starts with)\nreads as that field's default. Every default reproduces the UI's\nbehaviour from before the setting existed, except `table_density`, which\ndefaults to the tighter of its two layouts.", "properties": { "clock": { "$ref": "#/components/schemas/ClockFormat", "description": "12- or 24-hour clock for displayed times.", "default": "browser_default" }, "date_order": { "$ref": "#/components/schemas/DateOrder", "description": "Order of day, month and year in displayed dates.", "default": "browser_default" }, "table_density": { "$ref": "#/components/schemas/TableDensity", "description": "Cell padding and row height in entity tables.", "default": "compact" }, "time_zone": { "type": [ "string", "null" ], "description": "IANA time zone dates are shown in. `None` uses the browser's zone.", "default": null }, "timestamps": { "$ref": "#/components/schemas/TimestampStyle", "description": "Whether recent events show as relative (\"3h ago\") or absolute times.", "default": "relative" }, "week_start": { "$ref": "#/components/schemas/WeekStart", "description": "First day of the week in day pickers.", "default": "monday" } } }, "DockerInstall": { "type": "object", "description": "The docker install method.", "required": [ "env" ], "properties": { "compose": { "type": [ "string", "null" ], "description": "A ready-to-run `docker-compose.yml` for a first install. `None` for a reconfigure — the\noperator keeps their own compose and swaps in `env`, rather than replacing the whole file." }, "env": { "type": "array", "items": { "type": "string" }, "description": "The `SCANOPY_*` environment variables (`KEY=value`) this daemon is configured with. For a\nreconfigure these are exactly the vars that changed, so the UI can show them as a swap-in." } } }, "DockerUpgrade": { "type": "object", "description": "The docker upgrade method.", "required": [ "pull", "pinned_image" ], "properties": { "config_copy": { "type": [ "string", "null" ], "description": "Copies the daemon's config onto the `daemon-config` volume before the recreate. Set for\nimages that keep config outside the volume, where the recreate would otherwise erase it." }, "pinned_image": { "type": "string", "description": "The compose `image:` line pinned to this server's version." }, "pull": { "type": "string", "description": "Pulls and recreates on the `latest` tag." } } }, "DockerVirtualization": { "type": "object", "properties": { "compose_project": { "type": [ "string", "null" ], "description": "Compose project the container belongs to, when it was started by Compose." }, "container_id": { "type": [ "string", "null" ], "description": "Docker container ID." }, "container_name": { "type": [ "string", "null" ], "description": "Container name as reported by Docker." }, "state": { "$ref": "#/components/schemas/GuestState", "description": "Whether the container was running at the last scan." } } }, "Edge": { "allOf": [ { "$ref": "#/components/schemas/EdgeType", "description": "What relationship this edge represents, and the entities behind it." }, { "type": "object", "required": [ "id", "source", "target", "label", "source_handle", "target_handle", "is_multi_hop", "relation_key" ], "properties": { "endpoint_labels": { "type": [ "array", "null" ], "items": { "type": "string" }, "description": "The names of the two ends, source first, for edges that join two named things. The UI\ndraws each name at its own end; `label` carries the same names joined." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "is_multi_hop": { "type": "boolean", "description": "Whether the edge stands in for a path that crosses intermediate nodes." }, "label": { "type": [ "string", "null" ], "description": "Text drawn on the edge." }, "relation_key": { "type": [ "string", "null" ], "description": "Identity of the relation this edge stands for — see [`EdgeType::relation_key`]. Stamped\ncentrally from `edge_type` once the graph is built, so no construction site can forget\nit. `None` marks an edge as interchangeable with its like." }, "source": { "type": "string", "format": "uuid", "description": "Node the edge starts at." }, "source_handle": { "$ref": "#/components/schemas/EdgeHandle", "description": "Which side of the source node the edge leaves from." }, "target": { "type": "string", "format": "uuid", "description": "Node the edge ends at." }, "target_handle": { "$ref": "#/components/schemas/EdgeHandle", "description": "Which side of the target node the edge arrives at." }, "view_config": { "$ref": "#/components/schemas/EdgeViewConfig", "description": "Per-view overrides for how this edge is drawn." } } } ] }, "EdgeDefaultVisibility": { "type": "string", "description": "Whether an edge is visible by default or hidden behind a toggle", "enum": [ "visible", "hidden" ] }, "EdgeHandle": { "type": "string", "enum": [ "Top", "Bottom", "Left", "Right" ] }, "EdgeHighlightBehavior": { "type": "string", "description": "Controls when an edge contributes to node highlighting on selection", "enum": [ "when_visible", "always", "never" ] }, "EdgeStroke": { "type": "string", "description": "Visual stroke style for an edge", "enum": [ "solid", "dashed", "dotted" ] }, "EdgeStyle": { "type": "string", "enum": [ "Straight", "SmoothStep", "Bezier" ] }, "EdgeType": { "oneOf": [ { "type": "object", "title": "SameHost", "required": [ "host_id", "edge_type" ], "properties": { "edge_type": { "type": "string", "enum": [ "SameHost" ] }, "host_id": { "type": "string", "format": "uuid", "description": "The host both endpoints sit on." } } }, { "type": "object", "title": "Hypervisor", "required": [ "hypervisor_service_id", "edge_type" ], "properties": { "edge_type": { "type": "string", "enum": [ "Hypervisor" ] }, "hypervisor_service_id": { "type": "string", "format": "uuid", "description": "The hypervisor service running the guest." } } }, { "type": "object", "title": "NetworkIdentity", "description": "A guest's own address to one of its network identities on the same subnet: an address\nand MAC the guest presents from an interface of its own beyond its configured NICs.", "required": [ "identities_service_id", "edge_type" ], "properties": { "edge_type": { "type": "string", "enum": [ "NetworkIdentity" ] }, "identities_service_id": { "type": "string", "format": "uuid", "description": "The guest's Network Identities service, which owns the identity host." } } }, { "type": "object", "title": "ContainerRuntime", "required": [ "host_id", "service_id", "subnet_ids", "containerized_service_ids", "edge_type" ], "properties": { "containerized_service_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The containerized services this edge stands for — the ones on those subnets. Empty on\nan edge to a container host." }, "edge_type": { "type": "string", "enum": [ "ContainerRuntime" ] }, "host_id": { "type": "string", "format": "uuid", "description": "The host running the container runtime." }, "service_id": { "type": "string", "format": "uuid", "description": "The container runtime service itself." }, "subnet_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The bridge subnet(s) this edge reaches: one when they render as their own boxes,\nall of them when merged into a single box. Resolved here rather than in the\ninspector, which cannot tell which subnet an elevated edge landed on. For an edge to a\ncontainer host (macvlan, ipvlan), the subnet holding the address it ends on." } } }, { "type": "object", "title": "SameContainer", "description": "One container reachable at several of its host's container-bridge subnets. Ties the\ncontainer's addresses together so a multi-attached container reads as one thing rather\nthan as unrelated cards in separate subnet boxes.", "required": [ "service_id", "edge_type" ], "properties": { "edge_type": { "type": "string", "enum": [ "SameContainer" ] }, "service_id": { "type": "string", "format": "uuid", "description": "The containerized service reachable at several addresses." } } }, { "type": "object", "title": "RequestPath", "required": [ "dependency_id", "source_id", "target_id", "edge_type" ], "properties": { "dependency_id": { "type": "string", "format": "uuid", "description": "The dependency this edge was drawn from." }, "edge_type": { "type": "string", "enum": [ "RequestPath" ] }, "source_id": { "type": "string", "format": "uuid", "description": "Member the request starts at." }, "target_id": { "type": "string", "format": "uuid", "description": "Member the request arrives at." } } }, { "type": "object", "title": "HubAndSpoke", "required": [ "dependency_id", "source_id", "target_id", "edge_type" ], "properties": { "dependency_id": { "type": "string", "format": "uuid", "description": "The dependency this edge was drawn from." }, "edge_type": { "type": "string", "enum": [ "HubAndSpoke" ] }, "source_id": { "type": "string", "format": "uuid", "description": "The hub member." }, "target_id": { "type": "string", "format": "uuid", "description": "The spoke member." } } }, { "type": "object", "title": "PhysicalLink", "description": "Physical link discovered via LLDP/CDP neighbor discovery", "required": [ "source_entity_id", "target_entity_id", "protocol", "edge_type" ], "properties": { "edge_type": { "type": "string", "enum": [ "PhysicalLink" ] }, "protocol": { "$ref": "#/components/schemas/DiscoveryProtocol", "description": "Neighbour-discovery protocol the link was learned from." }, "source_entity_id": { "type": "string", "format": "uuid", "description": "Interface at one end of the cable." }, "target_entity_id": { "type": "string", "format": "uuid", "description": "Interface at the other end." } } }, { "type": "object", "title": "NeighborLink", "description": "Device-level adjacency from LLDP/CDP: the neighbour resolved to a host, but the remote\nport could not be pinned down (a locally-assigned port id that matches nothing, or a\nneighbour entry carrying no port id at all). The two devices are provably adjacent;\nwhich cables they meet on is unknown. `PhysicalLink` is the port-precise sibling.", "required": [ "source_host_id", "target_host_id", "protocol", "edge_type" ], "properties": { "edge_type": { "type": "string", "enum": [ "NeighborLink" ] }, "protocol": { "$ref": "#/components/schemas/DiscoveryProtocol", "description": "Neighbour-discovery protocol the adjacency was learned from." }, "source_host_id": { "type": "string", "format": "uuid", "description": "One of the adjacent devices." }, "target_host_id": { "type": "string", "format": "uuid", "description": "The other adjacent device." } } } ] }, "EdgeTypeDiscriminants": { "type": "string", "enum": [ "SameHost", "Hypervisor", "NetworkIdentity", "ContainerRuntime", "SameContainer", "RequestPath", "HubAndSpoke", "PhysicalLink", "NeighborLink" ] }, "EdgeViewConfig": { "oneOf": [ { "type": "object", "title": "Disabled", "description": "Edge is not available in this view", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "disabled" ] } } }, { "type": "object", "title": "Active", "description": "Edge is active in this view with specific properties", "required": [ "affects_layout", "default_visibility", "stroke", "highlight_behavior", "will_target_container", "show_directionality", "type" ], "properties": { "affects_layout": { "type": "boolean", "description": "Whether ELK should use this edge for layout positioning" }, "default_visibility": { "$ref": "#/components/schemas/EdgeDefaultVisibility", "description": "Whether the edge is shown by default or hidden behind a toggle" }, "highlight_behavior": { "$ref": "#/components/schemas/EdgeHighlightBehavior", "description": "When this edge contributes to node highlighting on selection" }, "show_directionality": { "type": "boolean", "description": "Whether this edge should show directional animation when highlighted" }, "stroke": { "$ref": "#/components/schemas/EdgeStroke", "description": "Visual stroke style" }, "type": { "type": "string", "enum": [ "active" ] }, "will_target_container": { "type": "boolean", "description": "Whether this edge should be elevated to target an accepting container\ninstead of the element inside it" } } } ], "description": "Per-view configuration for an edge: disabled (not in this view) or active with properties" }, "ElementEntityType": { "oneOf": [ { "type": "object", "title": "IPAddress", "required": [ "subnet_id", "element_type" ], "properties": { "element_type": { "type": "string", "enum": [ "IPAddress" ] }, "ip_address_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The IP address itself, when one is known." }, "subnet_id": { "type": "string", "format": "uuid", "description": "Subnet the address sits in." } } }, { "type": "object", "title": "Service", "required": [ "element_type" ], "properties": { "element_type": { "type": "string", "enum": [ "Service" ] } } }, { "type": "object", "title": "Host", "required": [ "element_type" ], "properties": { "element_type": { "type": "string", "enum": [ "Host" ] } } }, { "type": "object", "title": "Interface", "required": [ "interface_id", "element_type" ], "properties": { "element_type": { "type": "string", "enum": [ "Interface" ] }, "interface_id": { "type": "string", "format": "uuid", "description": "The interface this element stands for." } } } ] }, "ElementMark": { "type": "object", "description": "A colour a view paints on its element cards, named by the filter value that decodes it.\n\nThe colour is that value's `FilterValue::color`; the mark carries none of its own, so the card\nand the chip cannot disagree. A card is marked when its own entity, or its host, carries\n`value` under `filter_type`, judged by the extractor the filter's hover uses.", "required": [ "channel", "entity", "filter_type", "value" ], "properties": { "channel": { "$ref": "#/components/schemas/MarkChannel" }, "entity": { "$ref": "#/components/schemas/EntityDiscriminants", "description": "The entity whose filter value is read: the card's own, or `Host` for the card's host." }, "filter_type": { "$ref": "#/components/schemas/MetadataFilterType" }, "value": { "type": "string", "description": "The `FilterValue::id` that paints." } } }, "ElementSort": { "type": "string", "description": "The order of the nodes inside each top-level container, groups within it included.\n\nTop-level containers keep their own placement whatever the sort. Anything other than\n`Automatic` is applied by the server when it builds the graph, and the layout keeps that order\non screen, reading left to right, top to bottom.", "enum": [ "Automatic", "Address", "PortIndex", "Name", "Category" ] }, "EmailInstallCommandRequest": { "allOf": [ { "$ref": "#/components/schemas/InstallCommandQuery", "description": "The same purpose and advanced settings `GET /{id}/install-command` takes." }, { "type": "object", "required": [ "daemon_id", "os" ], "properties": { "api_key": { "type": [ "string", "null" ], "description": "The daemon's api key, from the provision response. Required when `purpose` embeds the\nkey (`install`, `rekey`) and rejected otherwise. The server stores only its hash, so it\ncan't fill the key in itself; it checks this one against the daemon's bound key." }, "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon whose command to send." }, "docker": { "type": "boolean", "description": "Send the docker compose instead of the binary command. Linux only." }, "os": { "$ref": "#/components/schemas/DaemonOs", "description": "Operating system the command targets." } } } ], "description": "Request body for emailing an install command to the authenticated user. The server builds\nthe command from the daemon record, exactly as [`get_install_command`] does; the client only\npicks which method's command to send." }, "EmailSettings": { "type": "object", "description": "Per-user toggles for the pausable email categories. Each field is named\nafter one [`EmailCategory`] whose `pausable()` is true; required\ncategories are never gated here.\n\nStored as a JSONB blob, so new categories are added as new fields rather\nthan via migration. New fields carry `#[serde(default = \"default_true\")]`\nso a category is opted in by default if its key is absent from the stored\nJSON.", "required": [ "discovery_digest" ], "properties": { "daemon_alerts": { "type": "boolean", "description": "Send an alert when a daemon stops reporting." }, "discovery_digest": { "type": "boolean", "description": "Send a periodic summary of what discovery found." }, "product_onboarding": { "type": "boolean", "description": "Send getting-started guidance." }, "product_updates": { "type": "boolean", "description": "Send the release notes when this server upgrades." }, "trial_and_usage": { "type": "boolean", "description": "Send trial reminders and plan-usage warnings." } } }, "EntitlementRequest": { "type": "object", "description": "Body the instance POSTs to [`ENTITLEMENT_PATH`].", "required": [ "key" ], "properties": { "instance_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The random id this server's database holds (`server_instance`), shared\nby every container on that database. Absent from servers older than\ninstance counting; those share one instance slot." }, "key": { "type": "string", "description": "The online key configured via `SCANOPY_LICENSE_KEY`." }, "non_production": { "type": [ "boolean", "null" ], "description": "Whether the server was started with `SCANOPY_NON_PRODUCTION`. Absent\nfrom servers older than the setting: they count as production, and\nthe cloud never refuses them a slot." } } }, "EntitlementResponse": { "type": "object", "description": "Data returned inside `ApiResponse` by [`ENTITLEMENT_PATH`].", "required": [ "entitlement" ], "properties": { "entitlement": { "type": "string", "description": "An offline-format license key (claims: [`super::types::LicenseClaims`])\nwith `org_id` and `plan` set from the organization's current state." }, "instances": { "oneOf": [ { "$ref": "#/components/schemas/InstanceUsage", "description": "How many server instances the key is running on, against how many it\ncovers. Absent from clouds older than instance counting." }, { "type": "null" } ] } } }, "EntityDiscriminants": { "type": "string", "enum": [ "Site", "Vlan", "Subnet", "Host", "Service", "IPAddress", "Interface", "Port", "Binding", "Discovery", "Daemon", "DaemonApiKey", "Tag", "TagGroup", "User", "UserApiKey", "Credential", "Organization", "Invite", "Topology", "Snapshot", "Share", "Dependency" ] }, "EntityField": { "type": "object", "description": "One fact that describes an entity, possibly read from a related entity.", "required": [ "id", "label", "group", "path", "format", "summary", "many" ], "properties": { "field": { "type": [ "string", "null" ], "description": "Dotted path to the value on the last object, or `None` for the object itself (an\n`Entity` or `Port` field)." }, "format": { "$ref": "#/components/schemas/FieldFormat" }, "group": { "$ref": "#/components/schemas/FieldGroup" }, "id": { "type": "string", "description": "Stable key: the CSV header, the list-table column id and the diagram Shape Data name." }, "label": { "type": "string", "description": "English label; the UI translates it through a generated `meta_*` key." }, "many": { "type": "boolean", "description": "Whether one entity can hold several values: set by any `Children` or `Embedded` hop, and\ndeclared with [`EntityField::many`] for a key holding an array of ids or values." }, "path": { "type": "array", "items": { "$ref": "#/components/schemas/Hop" }, "description": "Steps from the entity to the objects the value is read from. Empty for the entity itself." }, "source_field": { "type": [ "string", "null" ], "description": "Sibling field holding the value's attribution, for attributed host facts." }, "summary": { "type": "boolean", "description": "Shown by default in tables and in the inspector's details. Every field is exported." } } }, "EntityFreshness": { "type": "string", "description": "How recently discovery last observed an entity.\n\nDerived, never persisted — computed from `last_seen_at` against the\nentity's site staleness window (`Site::stale_cutoff`). Shared by the\ndiscovery digest email and the UI so a host reported stale in the digest is\nthe same host badged stale in the inventory and topology; running two\ndifferent measures let them disagree (a scan-count measure calls an entity\nmissing after 3 scans, which is 45 minutes on one site and 3 months on\nanother).\n\nOnly discovery-managed entities can be `Stale` — see\n[`DiscoveryTracked::is_discovery_managed`](crate::server::shared::storage::snapshot::DiscoveryTracked::is_discovery_managed).", "enum": [ "new", "current", "stale" ] }, "EntitySource": { "oneOf": [ { "type": "object", "title": "Manual", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Manual" ] } } }, { "type": "object", "title": "System", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "System" ] } } }, { "type": "object", "title": "Discovery", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Discovery" ] } } }, { "type": "object", "title": "DiscoveryWithMatch", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/MatchDetails" }, "type": { "type": "string", "enum": [ "DiscoveryWithMatch" ] } } }, { "type": "object", "title": "Inferred", "description": "Known only because something else reported it — an LLDP/CDP neighbour publishing an address,\nor a controller listing a device — and never contacted directly.\n\nA distinct rung rather than plain `Discovery` because the difference is exactly what an\noperator cannot otherwise see: a host with no ports, no services and an address nothing ever\nanswered on is indistinguishable from a device that is simply down. It counts as discovered\nfor every other purpose — see [`Self::is_from_discovery`] — so staleness, quota and the\ndiscovery FKs behave for it precisely as they do for a swept host.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Inferred" ] } } }, { "type": "object", "title": "Unknown", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Unknown" ] } } } ] }, "EntitySourceDiscriminants": { "type": "string", "enum": [ "Manual", "System", "Discovery", "DiscoveryWithMatch", "Inferred", "Unknown" ] }, "EsxiVirtualization": { "type": "object", "properties": { "vm_id": { "type": [ "string", "null" ], "description": "ESXi identifier of the guest." }, "vm_name": { "type": [ "string", "null" ], "description": "Guest name as configured on the ESXi host." } } }, "ExportFeatures": { "type": "object", "description": "Export feature flags derived from the share creator's billing plan", "required": [ "png_export", "svg_export", "mermaid_export", "confluence_export", "pdf_export", "html_export", "drawio_export", "vsdx_export", "remove_created_with" ], "properties": { "confluence_export": { "type": "boolean", "description": "Viewer may export the diagram for Confluence." }, "drawio_export": { "type": "boolean", "description": "Viewer may export the diagram as an editable draw.io file." }, "html_export": { "type": "boolean", "description": "Viewer may export the diagram as standalone HTML." }, "mermaid_export": { "type": "boolean", "description": "Viewer may export the diagram as Mermaid." }, "pdf_export": { "type": "boolean", "description": "Viewer may export the diagram as PDF." }, "png_export": { "type": "boolean", "description": "Viewer may export the diagram as PNG." }, "remove_created_with": { "type": "boolean", "description": "Exports omit the Scanopy attribution line." }, "svg_export": { "type": "boolean", "description": "Viewer may export the diagram as SVG." }, "vsdx_export": { "type": "boolean", "description": "Viewer may export the diagram as an editable Visio (VSDX) file." } } }, "FdbMacs": { "type": "object", "properties": { "fdb_macs": { "type": "array", "items": { "type": "string" }, "description": "MAC addresses the switch learned on this port. A port with one learned MAC can be resolved to a neighbor link; several indicate an uplink." }, "fdb_macs_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "FieldDefinition": { "type": "object", "description": "Definition of a form field for dynamic UI rendering.\n\nOne definition serves every dynamic form: credential types and discovery scan settings both\nbuild these, and the frontend renders them through the same switch. Reaches the UI through\nbuild-time fixtures (`credential-types.json`, `integrations.json`, `scan-settings.json`) and,\nfor the generated TypeScript union, through the OpenAPI schema.", "required": [ "id", "label", "field_type", "secret", "optional", "half_width" ], "properties": { "default_value": { "type": [ "string", "null" ], "description": "Value pre-filled when the field is first shown." }, "field_set": { "oneOf": [ { "$ref": "#/components/schemas/FieldSetRole", "description": "This field's part in a [`FieldSet`]: the dropdown that picks the set, or a field shown only\nfor some of its values. `None` for a field that always shows." }, { "type": "null" } ] }, "field_type": { "$ref": "#/components/schemas/FieldType", "description": "How the field should be rendered and validated." }, "file_name": { "type": [ "string", "null" ], "description": "For a field that can be read from a file on the daemon: an example file name, which the\nform joins to the Daemon OS's example directory for the path placeholder." }, "group": { "type": [ "string", "null" ], "description": "Grouping label used to section a long form." }, "half_width": { "type": "boolean", "description": "Whether the field is short enough to share a row. The form puts two adjacent half-width\nfields of the same group side by side." }, "help_text": { "type": [ "string", "null" ], "description": "Explanatory text shown beneath the field." }, "id": { "type": "string", "description": "Server-assigned unique identifier." }, "inline_format": { "oneOf": [ { "$ref": "#/components/schemas/InlineFormat", "description": "For `PathOrInline` and `SecretPathOrInline` fields: what format the inline value must be.\nAlso drives server-side validation in `CredentialType::validate`." }, { "type": "null" } ] }, "label": { "type": "string", "description": "Human-facing field label." }, "optional": { "type": "boolean", "description": "Whether the field may be left empty." }, "options": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/SelectOption" }, "description": "Choices for `Select` fields. Each option carries a wire `value` (the serialized enum\nvariant) and a human-facing `label`. A `FieldSet::Sets` dropdown takes them from its set's\nenum ([`FieldSetValue::options`])." }, "placeholder": { "type": [ "string", "null" ], "description": "Placeholder text for the input." }, "placeholder_by": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/DependentPlaceholder" }, "description": "Placeholders that replace `placeholder` while another field holds a given value, e.g. a\nWindows path once the OS picker says Windows. `field` is a sibling field id, or `daemon_os`\nfor the credential's own daemon OS." }, "secret": { "type": "boolean", "description": "Whether the value is a secret, so it is masked and never echoed back." } } }, "FieldFormat": { "oneOf": [ { "type": "object", "title": "Text", "description": "Plain text.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Text" ] } } }, { "type": "object", "title": "Prose", "description": "Free text a person wrote, such as a description. Tables wrap it rather than truncating.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Prose" ] } } }, { "type": "object", "title": "Number", "description": "A number, written as is.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Number" ] } } }, { "type": "object", "title": "Boolean", "description": "True or false.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Boolean" ] } } }, { "type": "object", "title": "Timestamp", "description": "A point in time, written as a date and time.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Timestamp" ] } } }, { "type": "object", "title": "Identifier", "description": "A value read as a code rather than prose (MAC, CIDR, chassis id): monospace, copyable.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Identifier" ] } } }, { "type": "object", "title": "BitsPerSecond", "description": "`speed_bps`, written as 1G / 100M.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "BitsPerSecond" ] } } }, { "type": "object", "title": "Port", "description": "A `{number, protocol}` port, written as 80/tcp.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Port" ] } } }, { "type": "object", "title": "OperatingSystem", "description": "A host `os` object, written as its name and version.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "OperatingSystem" ] } } }, { "type": "object", "title": "Metadata", "description": "An enum value, named through a metadata fixture.", "required": [ "fixture", "type" ], "properties": { "fixture": { "$ref": "#/components/schemas/MetadataFixture" }, "type": { "type": "string", "enum": [ "Metadata" ] } } }, { "type": "object", "title": "Entity", "description": "A related entity. The UI links it; CSV writes its id; text-only outputs write its display\nfield.", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "Entity" ] } } } ], "description": "How a field's value is written. Chosen by the value's shape, never by where it is shown." }, "FieldGroup": { "type": "string", "description": "The heading a field is listed under wherever fields are picked, such as a table's column\nmenu. One vocabulary for every entity: a MAC address is `Network` on hosts and interfaces\nalike. Variant order is display order.\n\nA field that points at other entities takes the first of these that fits:\n`Virtualization` when it says what virtualizes, contains or manages the entity; `Network`\nwhen the entities it reaches are addressing or connectivity (interfaces, IP addresses, ports,\nbindings, subnets, VLANs); `Relationships` otherwise. A host's services are\n`Relationships`: a service is what runs on the host, not how it is reached, and its ports\nalready show as the host's `Network` ports.", "enum": [ "identity", "network", "device", "virtualization", "relationships", "status", "configuration", "access", "discovery", "timestamps" ] }, "FieldSetRole": { "oneOf": [ { "type": "string", "description": "This dropdown picks the set.", "enum": [ "Sets" ] }, { "type": "object", "title": "Requires", "description": "Shown only while the dropdown holds one of these values.", "required": [ "Requires" ], "properties": { "Requires": { "type": "array", "items": { "type": "string" }, "description": "Shown only while the dropdown holds one of these values." } } } ], "description": "Schema of [`FieldDefinition::field_set`] on the wire. Never constructed: the values are the\nserialized variants of the set's own enum, which the schema cannot name generically." }, "FieldType": { "type": "string", "description": "Field types for dynamic form generation.\n\nThe serialized lowercase name is the wire contract with the frontend, which switches on it to\npick a renderer and a validator. Adding a variant means teaching the frontend about it.", "enum": [ "string", "text", "number", "boolean", "select", "radio", "port", "secretpathorinline", "pathorinline", "scriptsource" ] }, "FileOrInline": { "oneOf": [ { "type": "object", "title": "Inline", "required": [ "value", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "Inline" ] }, "value": { "type": "string", "description": "The value itself." } } }, { "type": "object", "title": "FilePath", "required": [ "path", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "FilePath" ] }, "path": { "$ref": "#/components/schemas/DaemonPath", "description": "Path to a file on the daemon host holding the value." } } } ], "description": "Non-secret value that can be inline content or a file path on daemon host." }, "FilterApplication": { "type": "string", "description": "Where a filter is applied — and therefore what toggling it costs.\n\nFiltering on the client makes a toggle instant, because the entities are already there. Filtering\non the server makes it a round trip, but keeps the hidden entities out of the response entirely.\nThat is a good trade only when the hidden set is large: L2 ships 19,095 interfaces to render\n2,872, and a customer's browser runs out of memory holding the difference.\n\n**A filter may only be `Server` if its value is computable from the topology the backend is\nalready building.** `Staleness` fails that test — it depends on the current time, so an identical\nrequest would return different results as time passes, and a cached response would be wrong. It\nstays `Client` permanently; this is a property of the filter, not a migration left half-done.", "enum": [ "Client", "Server" ] }, "FilterValue": { "type": "object", "required": [ "id", "label", "color", "icon" ], "properties": { "color": { "$ref": "#/components/schemas/Color", "description": "Colour shown on the filter chip." }, "icon": { "type": [ "string", "null" ], "description": "Icon shown on the filter chip." }, "id": { "type": "string", "description": "Server-assigned unique identifier." }, "label": { "type": "string", "description": "Human-facing label for this choice." } } }, "FinalizePaymentMethodRequest": { "type": "object", "description": "Request to finalize a client-confirmed SetupIntent (set the collected card\nas the customer's default payment method).", "required": [ "setup_intent_id" ], "properties": { "setup_intent_id": { "type": "string", "description": "Stripe SetupIntent to attach as the organization's payment method." } } }, "ForgotPasswordRequest": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "description": "Email address to send the password-reset link to." } } }, "GlobalSearchGroup": { "type": "object", "description": "One page of an entity type's matches.", "required": [ "entity_type", "total_count", "items" ], "properties": { "entity_type": { "$ref": "#/components/schemas/EntityDiscriminants" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/SearchHit" }, "description": "This page of matches, each in its entity's list shape." }, "total_count": { "type": "integer", "format": "int64", "description": "Matches of this type in all, across every page.", "minimum": 0 } } }, "GlobalSearchResponse": { "type": "object", "description": "Matches grouped by entity type, in registry order. Types with no matches are left out.", "required": [ "groups" ], "properties": { "groups": { "type": "array", "items": { "$ref": "#/components/schemas/GlobalSearchGroup" }, "description": "One group per entity type with at least one match." } } }, "GroupCount": { "type": "object", "description": "Size of one group in a grouped list, across every page.", "required": [ "count" ], "properties": { "count": { "type": "integer", "format": "int64", "description": "How many rows fall in this group in total, not just on this page.", "minimum": 0 }, "value": { "type": [ "string", "null" ], "description": "The group's value, rendered as text. `null` for rows whose group key is\nNULL (the \"ungrouped\" bucket)." } } }, "GuestState": { "type": "string", "description": "A guest's run state. Stored rows from before the state was recorded read as `Running`: Docker\nand Podman discovery listed running containers only, so every stored container was running.", "enum": [ "Running", "Paused", "Restarting", "Stopped" ] }, "Hop": { "oneOf": [ { "type": "object", "title": "Parent", "description": "The entities named by `key` on the current object: one id, or an array of ids.", "required": [ "entity", "key", "type" ], "properties": { "entity": { "$ref": "#/components/schemas/EntityDiscriminants" }, "key": { "type": "string", "description": "Field on the current object holding the parent's id or ids." }, "type": { "type": "string", "enum": [ "Parent" ] } } }, { "type": "object", "title": "Children", "description": "Every entity of type `entity` whose `key` holds the current object's id.", "required": [ "entity", "key", "type" ], "properties": { "entity": { "$ref": "#/components/schemas/EntityDiscriminants" }, "key": { "type": "string", "description": "Field on each child holding the current object's id." }, "type": { "type": "string", "enum": [ "Children" ] } } }, { "type": "object", "title": "Embedded", "description": "The array of objects held on the current object under `key`, such as a service's bindings.", "required": [ "key", "type" ], "properties": { "key": { "type": "string", "description": "Field on the current object holding the array." }, "type": { "type": "string", "enum": [ "Embedded" ] } } } ], "description": "One step from an object to related objects." }, "Host": { "allOf": [ { "$ref": "#/components/schemas/HostBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Discovery (historical row) that first observed this entity. Set once\n(post-terminal); immutable thereafter via the `IS NULL` guard in\n`update_discovery_fks`.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Discovery (historical row) that last touched this entity. Populated\npost-terminal by the per-entity-service subscriber on\n`DiscoveryProcessed`. NULL until the first successful discovery\nsession terminates after this row was created.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "Last successful natural-key match by daemon discovery against this\nlive row. Refreshed every scan, regardless of field changes.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Lineage pointer on closed historical rows back to the live row whose\nstate they capture. NULL on live rows.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "SCD2: when this row version became live. Equal to `created_at` for\nrows that have never ridden a snapshot; advanced to the snapshot's\n`taken_at` for live rows after a site snapshot fires.", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "SCD2: when this row was closed by a snapshot. NULL = currently live.", "readOnly": true } } } ], "example": { "asset_tag": "IT-00412", "asset_tag_source": { "Probe": "Snmp" }, "created_at": "2026-01-15T10:30:00Z", "credential_assignments": [], "description": "Primary web server", "first_discovery_id": null, "hostname": "web-server-01.local", "hostname_source": "Manual", "id": "550e8400-e29b-41d4-a716-446655440003", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "web-server-01", "name_source": "Manual", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_interface_id": null, "virtualization_metadata": null, "virtualization_service_id": null } }, "HostAssetTag": { "type": "object", "properties": { "asset_tag": { "type": "string", "description": "ENTITY-MIB entPhysicalAssetID - the organization's asset tag" }, "asset_tag_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostBase": { "allOf": [ { "$ref": "#/components/schemas/HostName", "description": "The host's name, together with the rung of the naming ladder that produced it.\n\nSerialises as the two flat keys `name` and `name_source`, so the wire format is a bare\nstring exactly as it has always been. Assign only through [`HostBase::apply_name`]." }, { "$ref": "#/components/schemas/HostHostname", "description": "The host's hostname, with the source that produced it.\n\nAn identifier rather than a name, so it is never copied into `name`: see the placement rule\nin [`super::name`]. Two flat wire keys, `hostname` and `hostname_source`, so a daemon that\nsends only `hostname` still deserialises, at `Unspecified`." }, { "$ref": "#/components/schemas/HostSysDescr", "description": "SNMP sysDescr.0 - full system description" }, { "$ref": "#/components/schemas/HostSysObjectId", "description": "SNMP sysObjectID.0 - vendor OID for device identification" }, { "$ref": "#/components/schemas/HostSysLocation", "description": "SNMP sysLocation.0 - physical location" }, { "$ref": "#/components/schemas/HostSysContact", "description": "SNMP sysContact.0 - admin contact info" }, { "$ref": "#/components/schemas/HostManagementUrl", "description": "URL for device management interface (manual or discovered)" }, { "$ref": "#/components/schemas/HostChassisId", "description": "LLDP lldpLocChassisId - globally unique device identifier for deduplication" }, { "$ref": "#/components/schemas/HostSysName", "description": "SNMP sysName.0 - administratively-assigned hostname" }, { "$ref": "#/components/schemas/HostManufacturer", "description": "ENTITY-MIB entPhysicalMfgName - hardware manufacturer" }, { "$ref": "#/components/schemas/HostModel", "description": "ENTITY-MIB entPhysicalModelName - hardware model" }, { "$ref": "#/components/schemas/HostSerialNumber", "description": "ENTITY-MIB entPhysicalSerialNum - hardware serial number" }, { "$ref": "#/components/schemas/HostAssetTag", "description": "The organization's asset tag: ENTITY-MIB entPhysicalAssetID, or typed in by a person." }, { "$ref": "#/components/schemas/HostFirmwareRevision", "description": "Firmware revision of the device as a whole — ENTITY-MIB `entPhysicalFirmwareRev`.\n\nWritten by whichever source read it — a controller's REST inventory, an industrial probe's\nidentity response, and `entPhysicalFirmwareRev`. Before this column existed each of those\nhad nowhere to put a version it had already read, and two of them flattened it into\n`sys_descr` as prose.\n\nDevice-level rather than per-module: everything downstream is host-shaped, and the NCCoE\nasset-inventory minimum says \"product version\", singular." }, { "$ref": "#/components/schemas/HostSoftwareRevision", "description": "Software revision of the device as a whole — ENTITY-MIB `entPhysicalSoftwareRev`.\n\nIts own field rather than sharing `firmware_revision`, because RFC 4133 defines `.9` and\n`.10` as distinct objects: on a Cisco chassis they are the bootloader and the IOS version,\nand one column holding either with nothing recording which cannot tell them apart. Only\nENTITY-MIB writes it — every other source reports a single version, which is the firmware." }, { "$ref": "#/components/schemas/HostOperatingSystem", "description": "The host's operating system, from whichever source read or matched it. See\n[`HostOsValue`](super::attributes::HostOsValue) for who writes it and how they rank." }, { "type": "object", "required": [ "site_id", "description", "source", "virtualization_metadata", "virtualization_service_id", "virtualization_interface_id", "tags", "credential_assignments" ], "properties": { "credential_assignments": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialAssignment" }, "description": "Credential assignments for this host (hydrated from junction table)." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the host." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "How this host came to be known — discovered, imported, or created by hand.", "readOnly": true }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "virtualization_interface_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The interface on the virtualizing host that presents this one, for a host whose\nvirtualizer presents it from an interface of its own (a network identity). Its own column\nwith a foreign key for the same reason as `virtualization_service_id`; `ON DELETE SET NULL`\nclears it when the interface goes away." }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest. A provider from a newer\ndaemon reads as `None` instead of rejecting the host." }, { "type": "null" } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The service doing the virtualizing — the hypervisor this VM runs on.\n\nIts own column with a foreign key rather than a field inside\n[`HostVirtualization`]: a reference that no longer resolves now fails the write instead of\nsurviving as a value nothing matches, and `ON DELETE SET NULL` clears it when the\nhypervisor service goes away (GH #650)." } } } ], "description": "Base data for a Host entity (stored in database).\nChild entities (ip_addresses, ports, services) are stored in their own tables\nand queried by `host_id`. They are NOT stored on the host." }, "HostChassisId": { "type": "object", "properties": { "chassis_id": { "type": "string", "description": "LLDP lldpLocChassisId - globally unique device identifier for deduplication" }, "chassis_id_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostFirmwareRevision": { "type": "object", "properties": { "firmware_revision": { "type": "string", "description": "ENTITY-MIB entPhysicalFirmwareRev - firmware revision of the device as a whole" }, "firmware_revision_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostHostname": { "type": "object", "properties": { "hostname": { "type": "string", "description": "Hostname as resolved or reported for the host" }, "hostname_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostManagementUrl": { "type": "object", "properties": { "management_url": { "type": "string", "description": "URL for device management interface (manual or discovered)" }, "management_url_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostManufacturer": { "type": "object", "properties": { "manufacturer": { "type": "string", "description": "ENTITY-MIB entPhysicalMfgName - hardware manufacturer" }, "manufacturer_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostModel": { "type": "object", "properties": { "model": { "type": "string", "description": "ENTITY-MIB entPhysicalModelName - hardware model" }, "model_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostName": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "Human-facing name for the host." }, "name_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostNameLadderEntry": { "type": "object", "description": "What one rung of the ladder holds for a particular host.", "required": [ "rung", "value", "source" ], "properties": { "rung": { "$ref": "#/components/schemas/HostNameRung" }, "source": { "oneOf": [ { "$ref": "#/components/schemas/AttributeSource", "description": "Where the value came from. `None` when there is no value, and for `Address`, whose column\ncarries no provenance." }, { "type": "null" } ] }, "value": { "type": [ "string", "null" ], "description": "The rung's value, or `None` when the host has nothing there. A value holding only\nwhitespace counts as nothing." } } }, "HostNameRung": { "type": "string", "description": "One rung of the display-name ladder.", "enum": [ "Name", "Hostname", "SysName", "ChassisId", "Address" ] }, "HostNamingFallback": { "type": "string", "enum": [ "Ip", "BestService" ] }, "HostOperatingSystem": { "type": "object", "properties": { "os": { "type": "object", "description": "Operating system of a host, as one source read or inferred it.", "required": [ "family" ], "properties": { "codename": { "type": [ "string", "null" ], "description": "Release codename, such as \"noble\"." }, "edition": { "type": [ "string", "null" ], "description": "Edition, such as \"LTS\" or \"Datacenter\"." }, "family": { "$ref": "#/components/schemas/HostOsFamily", "description": "The operating system family." }, "kernel_version": { "type": [ "string", "null" ], "description": "Kernel release, such as \"6.8.0-45-generic\"." }, "name": { "type": [ "string", "null" ], "description": "Product or distribution, such as \"Ubuntu\", \"Windows Server 2022\" or \"IOS-XE\"." }, "version": { "type": [ "string", "null" ], "description": "Release, such as \"24.04\", \"10.0.20348\" or \"15.2(4)M\"." } } }, "os_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostOrderField": { "type": "string", "description": "Fields that hosts can be ordered/grouped by.", "enum": [ "created_at", "name", "hostname", "updated_at", "virtualized_by", "site_id", "interface_ip", "last_seen_at", "mac_address", "source", "manufacturer", "model", "sys_location", "os_family" ] }, "HostOs": { "type": "object", "description": "Operating system of a host, as one source read or inferred it.", "required": [ "family" ], "properties": { "codename": { "type": [ "string", "null" ], "description": "Release codename, such as \"noble\"." }, "edition": { "type": [ "string", "null" ], "description": "Edition, such as \"LTS\" or \"Datacenter\"." }, "family": { "$ref": "#/components/schemas/HostOsFamily", "description": "The operating system family." }, "kernel_version": { "type": [ "string", "null" ], "description": "Kernel release, such as \"6.8.0-45-generic\"." }, "name": { "type": [ "string", "null" ], "description": "Product or distribution, such as \"Ubuntu\", \"Windows Server 2022\" or \"IOS-XE\"." }, "version": { "type": [ "string", "null" ], "description": "Release, such as \"24.04\", \"10.0.20348\" or \"15.2(4)M\"." } } }, "HostOsFamily": { "type": "string", "description": "Operating system family of a host.\n\nNo catch-all variant. A source that names something outside this list writes no OS at all, so\nnothing downstream has to handle a family that says nothing. The list covers every OS family the\nvendored Recog fingerprints name (`recog::tests::every_vendored_family_is_classified` keeps it\nthat way), plus every OS a daemon can report about its own host. Linux distributions are `Linux`\nwith the distribution as the name; variants exist for families, not distributions.", "enum": [ "Linux", "Windows", "MacOs", "Ios", "TvOs", "AudioOs", "FreeBsd", "OpenBsd", "NetBsd", "Solaris", "Aix", "HpUx", "Irix", "Tru64", "OpenVms", "ZOs", "IbmI", "UnixWare", "OpenServer", "NetWare", "PalmOs", "Esxi", "CiscoIos", "CiscoIosXe", "CiscoIosXr", "CiscoNxOs", "CiscoCatOs", "CiscoAsa", "CiscoFtd", "Junos", "ScreenOs", "ArubaOs", "Comware", "ProCurve", "AristaEos", "RouterOs", "HuaweiVrp", "IronWare", "BrocadeNetworkOs", "BayRs", "PanOs", "FortiOs", "SonicOs", "GaiaOs", "Ipso", "DataOntap", "Ecos" ] }, "HostPath": { "type": "string", "description": "A file on the scanned host, executed there over SSH. Never read by the daemon." }, "HostResponse": { "type": "object", "description": "Response type for host endpoints.\nIncludes children (ip_addresses, ports, services, interfaces).", "required": [ "id", "created_at", "updated_at", "last_seen_at", "name", "site_id", "source", "hidden", "tags", "firmware_revision", "software_revision", "os", "ip_addresses", "ports", "services", "interfaces" ], "properties": { "asset_tag": { "type": [ "string", "null" ], "description": "The asset tag the device reports: ENTITY-MIB entPhysicalAssetID. Read-only: discovery is\nthe only writer, so it always says what the device itself carries.", "readOnly": true }, "asset_tag_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the asset tag. Read-only.", "readOnly": true }, "chassis_id": { "type": [ "string", "null" ], "description": "LLDP chassis identifier, used to match the host to its neighbours." }, "chassis_id_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the LLDP chassis identifier. Read-only: decided by whichever source read it.", "readOnly": true }, "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "credential_assignments": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialAssignment" }, "description": "Credentials assigned to scan this host." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the host." }, "device_type": { "$ref": "#/components/schemas/DeviceType", "description": "What this host is, as a device: the role its icon shows everywhere it appears. Computed\nby [`Host::device_type`] from its services, then its OS, as the topology computes it.\nRead-only.", "readOnly": true }, "display_name": { "type": [ "string", "null" ], "description": "What to call this host when `name` is empty: its hostname, sysName, chassis id or first\naddress, whichever it has. `None` when nothing identifies it.\n\nRead-only and separate from `name` rather than folded into it. `name` is what a person\ntyped and what the editor writes back, so filling it with a fallback would turn a chassis\nid into a name the next save persists. Computed from [`Host::display_name`], the same\nladder topology titles a host container with, so a host cannot be called one thing in the\nlist and another on the map.", "readOnly": true }, "display_name_rung": { "oneOf": [ { "$ref": "#/components/schemas/HostNameRung", "description": "Which rung of the ladder produced `display_name`. `None` exactly when `display_name` is.\n\nResolved from the same [`Host::name_ladder`] call as `display_name`, so the UI can say\nwhere a host's title came from without walking the rungs itself." }, { "type": "null" } ], "readOnly": true }, "firmware_revision": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalFirmwareRev — firmware revision of the device. Read-only, as above.", "readOnly": true }, "firmware_revision_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the firmware revision. Read-only: decided by whichever source read it.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery run that first observed this host. Drives the \"First found by\" column.", "readOnly": true }, "hidden": { "type": "boolean", "description": "Always `false`. Kept because daemons before v0.17.23 require it when they read a host\nback; a host is hidden from topology by a tag of the Visibility group.", "readOnly": true }, "hostname": { "type": [ "string", "null" ], "description": "Hostname as resolved or reported by the host." }, "hostname_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced `hostname`: a PTR lookup, the host's own OS, a controller, mDNS, or a person.\nRead-only: decided by whichever source read it.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/Interface" }, "description": "SNMP ifTable entries" }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "IP addresses on this host." }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery run that last observed this host. Drives the \"Last found by\" column.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "Last time discovery observed this host. User-facing (drives the \"Last\nseen\" column and the stale badge), which is why it is carried here while\nthe version-history columns are not." }, "management_url": { "type": [ "string", "null" ], "description": "Link to the host's own management interface." }, "management_url_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the management URL. Read-only: decided by whichever source read it.", "readOnly": true }, "manufacturer": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalMfgName — hardware manufacturer. Read-only, as above.", "readOnly": true }, "manufacturer_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the manufacturer. Read-only: decided by whichever source read it.", "readOnly": true }, "model": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalModelName — hardware model. Read-only, as above.", "readOnly": true }, "model_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the model. Read-only: decided by whichever source read it.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for the host." }, "name_ladder": { "type": "array", "items": { "$ref": "#/components/schemas/HostNameLadderEntry", "readOnly": true }, "description": "Every rung of the display-name ladder for this host, highest first, with what each holds." }, "name_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced `name`. Read-only: it is decided by whoever supplied the name, not by the\ncaller.", "readOnly": true }, "os": { "oneOf": [ { "$ref": "#/components/schemas/HostOs", "description": "The host's operating system. Read-only: written by whichever source read or matched it.\nLenient, so a daemon tolerates a family added on a newer server." }, { "type": "null" } ], "readOnly": true }, "os_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the OS. Read-only, as above. A source whose method is `Inferred` matched the OS\nin a string the host emitted rather than reading it off the host.", "readOnly": true }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/Port" }, "description": "Open ports on this host." }, "serial_number": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalSerialNum — hardware serial number. Read-only, as above.", "readOnly": true }, "serial_number_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the serial number. Read-only: decided by whichever source read it.", "readOnly": true }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Services running on this host." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "software_revision": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalSoftwareRev — software revision of the device. Read-only, as above.", "readOnly": true }, "software_revision_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the software revision. Read-only: decided by whichever source read it.", "readOnly": true }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "How this host came to be known — discovered, imported, or created by hand." }, "sys_contact": { "type": [ "string", "null" ], "description": "SNMP sysContact — administrative contact as configured on the device." }, "sys_contact_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysContact. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_descr": { "type": [ "string", "null" ], "description": "SNMP sysDescr — the device's own description of itself." }, "sys_descr_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysDescr. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_location": { "type": [ "string", "null" ], "description": "SNMP sysLocation — physical location as configured on the device." }, "sys_location_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysLocation. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_name": { "type": [ "string", "null" ], "description": "SNMP sysName.0 — the administratively-assigned hostname. Read-only: discovery collects it\nfrom the device, so neither create nor update accepts it.", "readOnly": true }, "sys_name_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysName. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_object_id": { "type": [ "string", "null" ], "description": "SNMP sysObjectID — the vendor's identifier for the device model." }, "sys_object_id_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysObjectID. Read-only: decided by whichever source read it.", "readOnly": true }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "virtualization_depth": { "type": "integer", "format": "int32", "description": "How many hosts sit above this one in its virtualization chain.", "readOnly": true, "minimum": 0 }, "virtualization_interface_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The interface on the virtualizing host that presents this host, for a network identity." }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." }, { "type": "null" } ] }, "virtualization_parent_host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The host this one runs under: the host of its virtualizing service.", "readOnly": true }, "virtualization_root_host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The host at the top of this host's virtualization chain, itself when it is the top.\n`null` when the host neither runs under nor runs another host.", "readOnly": true }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } }, "example": { "asset_tag": "IT-00412", "asset_tag_source": { "Probe": "Snmp" }, "chassis_id_source": "Unspecified", "created_at": "2026-01-15T10:30:00Z", "credential_assignments": [], "description": "Primary web server", "device_type": "Server", "display_name": "web-server-01", "display_name_rung": "Name", "firmware_revision": null, "firmware_revision_source": "Unspecified", "first_discovery_id": null, "hidden": false, "hostname": "web-server-01.local", "hostname_source": "Manual", "id": "550e8400-e29b-41d4-a716-446655440003", "interfaces": [ { "admin_status": "Up", "created_at": "2026-01-15T10:30:00Z", "display_name": "Uplink to Core Switch", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-44665544000f", "if_alias": "Uplink to Core Switch", "if_descr": "GigabitEthernet0/1", "if_index": 1, "if_name": "Gi0/1", "if_type": 6, "interface_source": { "Probe": "Snmp" }, "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "ip_configured": true, "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "mac_address_source": "ArpReply", "neighbor_candidates": [], "oper_status": "Up", "site_id": "550e8400-e29b-41d4-a716-446655440002", "speed_bps": 1000000000, "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "vlan_membership_source": "Unspecified" } ], "ip_addresses": [ { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440005", "ip_address": "192.168.1.100", "ip_address_source": "ArpReply", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "mac_address_source": "ArpReply", "name": "eth0", "position": 0, "site_id": "550e8400-e29b-41d4-a716-446655440002", "subnet_id": "550e8400-e29b-41d4-a716-446655440004", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "management_url_source": "Unspecified", "manufacturer_source": "Unspecified", "model_source": "Unspecified", "name": "web-server-01", "name_ladder": [ { "rung": "Name", "source": "Manual", "value": "web-server-01" }, { "rung": "Hostname", "source": "Manual", "value": "web-server-01.local" }, { "rung": "SysName", "source": null, "value": null }, { "rung": "ChassisId", "source": null, "value": null }, { "rung": "Address", "source": null, "value": "192.168.1.100" } ], "name_source": "Manual", "os": null, "os_source": "Unspecified", "ports": [ { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440006", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "number": 80, "port_source": "PortScan", "protocol": "Tcp", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "serial_number_source": "Unspecified", "services": [ { "bindings": [ { "created_at": "2026-10-22T23:24:30.797110Z", "first_discovery_id": null, "id": "3d638c11-1399-42ba-ad25-99cc4c837064", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-10-22T23:24:30.797110Z", "lineage_id": null, "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Port", "updated_at": "2026-10-22T23:24:30.797110Z", "valid_from": "2026-10-22T23:24:30.797110Z", "valid_to": null } ], "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440007", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "nginx", "position": 0, "service_definition": "Consul", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_metadata": null, "virtualization_service_id": null } ], "site_id": "550e8400-e29b-41d4-a716-446655440002", "software_revision": null, "software_revision_source": "Unspecified", "source": { "type": "Manual" }, "sys_contact_source": "Unspecified", "sys_descr_source": "Unspecified", "sys_location_source": "Unspecified", "sys_name_source": "Unspecified", "sys_object_id_source": "Unspecified", "tags": [], "updated_at": "2026-01-15T10:30:00Z", "virtualization_depth": 0, "virtualization_interface_id": null, "virtualization_metadata": null, "virtualization_parent_host_id": null, "virtualization_root_host_id": null, "virtualization_service_id": null } }, "HostSerialNumber": { "type": "object", "properties": { "serial_number": { "type": "string", "description": "ENTITY-MIB entPhysicalSerialNum - hardware serial number" }, "serial_number_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostSoftwareRevision": { "type": "object", "properties": { "software_revision": { "type": "string", "description": "ENTITY-MIB entPhysicalSoftwareRev - software revision of the device as a whole" }, "software_revision_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostSysContact": { "type": "object", "properties": { "sys_contact": { "type": "string", "description": "SNMP sysContact.0 - admin contact info" }, "sys_contact_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostSysDescr": { "type": "object", "properties": { "sys_descr": { "type": "string", "description": "SNMP sysDescr.0 - full system description" }, "sys_descr_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostSysLocation": { "type": "object", "properties": { "sys_location": { "type": "string", "description": "SNMP sysLocation.0 - physical location" }, "sys_location_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostSysName": { "type": "object", "properties": { "sys_name": { "type": "string", "description": "SNMP sysName.0 - administratively-assigned hostname" }, "sys_name_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostSysObjectId": { "type": "object", "properties": { "sys_object_id": { "type": "string", "description": "SNMP sysObjectID.0 - vendor OID for device identification" }, "sys_object_id_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "HostVirtualization": { "oneOf": [ { "type": "object", "title": "Proxmox", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/ProxmoxVirtualization" }, "type": { "type": "string", "enum": [ "Proxmox" ] } } }, { "type": "object", "title": "VCenter", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/VCenterVirtualization" }, "type": { "type": "string", "enum": [ "VCenter" ] } } }, { "type": "object", "title": "ESXi", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/EsxiVirtualization" }, "type": { "type": "string", "enum": [ "ESXi" ] } } }, { "type": "object", "title": "Docker", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/ContainerHostVirtualization" }, "type": { "type": "string", "enum": [ "Docker" ] } } }, { "type": "object", "title": "Podman", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/ContainerHostVirtualization" }, "type": { "type": "string", "enum": [ "Podman" ] } } }, { "type": "object", "title": "NetworkIdentity", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/NetworkIdentityVirtualization" }, "type": { "type": "string", "enum": [ "NetworkIdentity" ] } } } ], "title": "HostVirtualization" }, "HostVirtualizationState": { "type": "string", "description": "Coarse virtualization state used by the `Virtualization` metadata filter on Host. Each host\nresolves to exactly one variant via `HasFilterValues`, from the type of its\n`virtualization_metadata` ([`HostVirtualizationDiscriminants::state`]).\n\nA container host and a network identity get states of their own rather than `Virtualized`:\nthe filter's colour marks Workloads cards, and an identity is an interface its owner presents,\nnot a guest it runs. The ids are persisted in hide-sets, so existing ones keep their spelling.", "enum": [ "Virtualized", "Containerized", "NetworkIdentity", "BareMetal" ] }, "IPAddress": { "allOf": [ { "$ref": "#/components/schemas/IPAddressBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440005", "ip_address": "192.168.1.100", "ip_address_source": "ArpReply", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "mac_address_source": "ArpReply", "name": "eth0", "position": 0, "site_id": "550e8400-e29b-41d4-a716-446655440002", "subnet_id": "550e8400-e29b-41d4-a716-446655440004", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "IPAddressBase": { "allOf": [ { "$ref": "#/components/schemas/MacEvidence", "description": "MAC address discovered from ARP, SNMP or a container runtime, with the evidence for it." }, { "type": "object", "required": [ "site_id", "host_id", "subnet_id", "ip_address", "name" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "ip_address": { "type": "string", "description": "IPv4 or IPv6 address.", "example": "192.168.1.10" }, "ip_address_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What found this address on the host." }, "name": { "type": [ "string", "null" ], "description": "Human-facing name for this IP address." }, "position": { "type": "integer", "format": "int32", "description": "Position of this IP address in the host's IP address list (for ordering)" }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "subnet_id": { "type": "string", "format": "uuid", "description": "The subnet this entity belongs to." } } } ] }, "IPAddressInput": { "type": "object", "description": "Input for creating or updating an interface.\nUsed in both CreateHostRequest and UpdateHostRequest.\nClient must provide a UUID for the interface.", "required": [ "id", "subnet_id", "ip_address" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Client-provided UUID for this interface" }, "ip_address": { "type": "string", "description": "IPv4 or IPv6 address.", "example": "192.168.1.10" }, "mac_address": { "type": [ "string", "null" ], "description": "MAC address, when known.", "example": "a4:bb:6d:12:34:56", "pattern": "^(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$" }, "name": { "type": [ "string", "null" ], "description": "Human-facing name for this IP address." }, "position": { "type": [ "integer", "null" ], "format": "int32", "description": "Position in the host's interface list (for ordering).\nIf omitted on create: appends to end of list.\nIf omitted on update: existing ip_addresses keep their positions; new ip_addresses append.\nMust be all specified or all omitted across all ip_addresses in the request." }, "subnet_id": { "type": "string", "format": "uuid", "description": "The subnet this entity belongs to." } } }, "IdentifiedRule_ContainerRule": { "type": "object", "description": "Generic wrapper that gives any rule type a stable UUID identity.", "required": [ "id", "rule" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "rule": { "oneOf": [ { "type": "string", "title": "BySubnet", "description": "One container per subnet.", "enum": [ "BySubnet" ] }, { "type": "string", "title": "MergeContainerBridges", "description": "Draw a host's container bridges as a single box rather than one each.", "enum": [ "MergeContainerBridges" ] }, { "type": "object", "title": "ByApplication", "description": "One container per application tag.", "required": [ "ByApplication" ], "properties": { "ByApplication": { "type": "object", "description": "One container per application tag.", "properties": { "tag_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Application tags to draw containers for. Empty means every application tag." } } } } }, { "type": "string", "title": "ByHost", "description": "One container per host.", "enum": [ "ByHost" ] } ], "description": "Rules that change which containers exist and how they nest.\nContainer titles are data-driven (subnet CIDR, host names), not user-configurable." } } }, "IdentifiedRule_ElementRule": { "type": "object", "description": "Generic wrapper that gives any rule type a stable UUID identity.", "required": [ "id", "rule" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "rule": { "oneOf": [ { "type": "object", "title": "ByServiceCategory", "description": "One subcontainer per group of service categories.", "required": [ "ByServiceCategory" ], "properties": { "ByServiceCategory": { "type": "object", "description": "One subcontainer per group of service categories.", "required": [ "categories" ], "properties": { "categories": { "type": "array", "items": { "$ref": "#/components/schemas/ServiceCategory" }, "description": "Service categories to group into this subcontainer.\n\nLenient on read so a category this build does not know drops out of the rule instead\nof failing the whole topology — see [`deserialize_known_categories`]." }, "is_infra_rule": { "type": "boolean", "description": "Set by the backend on the default infrastructure rule.\nFrontend uses this to identify the infra container for auto-collapse.", "readOnly": true }, "title": { "type": [ "string", "null" ], "description": "Heading for the subcontainer. Defaults to the category name." } } } } }, { "type": "object", "title": "ByTag", "description": "One subcontainer per group of tags.", "required": [ "ByTag" ], "properties": { "ByTag": { "type": "object", "description": "One subcontainer per group of tags.", "required": [ "tag_ids" ], "properties": { "tag_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags to group into this subcontainer." }, "title": { "type": [ "string", "null" ], "description": "Heading for the subcontainer. Defaults to the tag name." } } } } }, { "type": "string", "enum": [ "ByHypervisor" ] }, { "type": "string", "enum": [ "ByContainerRuntime" ] }, { "type": "string", "enum": [ "ByStack" ] }, { "type": "string", "description": "Groups trunk ports (ports with tagged VLANs) into a \"Trunk Ports\" subcontainer.\nHigher priority than ByVLAN — prevents trunk ports from being grouped by VLAN.", "enum": [ "ByTrunkPort" ] }, { "type": "string", "description": "Groups access ports by their native VLAN ID into per-VLAN subcontainers.", "enum": [ "ByVLAN" ] }, { "type": "string", "description": "Groups ports by operational status (Up, Down, etc.) into per-status subcontainers.", "enum": [ "ByPortOpStatus" ] } ], "description": "Rules that organize nodes within a container into sub-groups." } } }, "IfAdminStatus": { "type": "string", "description": "SNMP ifAdminStatus values per IF-MIB RFC 2863", "enum": [ "Up", "Down", "Testing" ] }, "IfOperStatus": { "type": "string", "description": "SNMP ifOperStatus values per IF-MIB RFC 2863", "enum": [ "Up", "Down", "Testing", "Unknown", "Dormant", "NotPresent", "LowerLayerDown" ] }, "InlineFormat": { "type": "string", "description": "Format hint for inline values in `PathOrInline` and `SecretPathOrInline` fields.", "enum": [ "plain", "pemprivatekey", "pemcertificate", "sshprivatekey", "macaddress", "proxmoxtokenid" ] }, "InlineGroup": { "type": "object", "description": "Visual grouping metadata for inlined entities.\nEntities sharing the same `group_id` are rendered together in the element card.", "required": [ "entity_id", "entity_type", "group_id", "role" ], "properties": { "entity_id": { "type": "string", "format": "uuid", "description": "The inlined entity's ID (e.g., service ID)." }, "entity_type": { "$ref": "#/components/schemas/EntityDiscriminants", "description": "Which entity `entity_id` names: a service, or a host (a guest's network identity)." }, "group_id": { "type": "string", "format": "uuid", "description": "Shared by all members of the visual group." }, "role": { "$ref": "#/components/schemas/InlineGroupRole", "description": "Whether this entity heads the inline group or is a member of it." } } }, "InlineGroupRole": { "type": "string", "description": "Role of an inlined entity within its visual group.", "enum": [ "Header", "Member" ] }, "InspectorSection": { "type": "string", "description": "A section that can appear in the inspector panel", "enum": [ "Identity", "Services", "Dependencies", "HostDetail", "Virtualization", "OtherInterfaces", "PortBindings", "ElementSummary", "DependencySummary", "Application" ] }, "InstallArtifacts": { "type": "object", "description": "Everything the UI needs to install (or reconfigure) a daemon, one field per install method so\neach is a first-class peer with its own content — no method is a special case bolted onto a\nlist. The binary methods are ready-to-paste commands (any api key is the [`API_KEY_PLACEHOLDER`],\nfilled in client-side); docker and msi carry their own structured content.", "required": [ "linux", "macos", "windows", "freebsd", "docker", "msi" ], "properties": { "docker": { "$ref": "#/components/schemas/DockerInstall", "description": "Container image reference." }, "freebsd": { "type": "string", "description": "Download for FreeBSD." }, "linux": { "type": "string", "description": "Download for Linux." }, "macos": { "type": "string", "description": "Download for macOS." }, "msi": { "$ref": "#/components/schemas/MsiInstall", "description": "Windows installer package." }, "windows": { "type": "string", "description": "Download for Windows." } } }, "InstallCommandQuery": { "type": "object", "description": "Query for [`get_install_command`]. `purpose` is required; the rest are the client-settable\nadvanced daemon settings, folded into an `install` command. Lists are comma-joined\n(`interfaces`, `credential_refs`).", "required": [ "purpose" ], "properties": { "accept_invalid_scan_certs": { "type": [ "boolean", "null" ], "description": "Continue scanning targets that present an untrusted certificate." }, "allow_self_signed_certs": { "type": [ "boolean", "null" ], "description": "Accept a self-signed certificate when connecting back to the server." }, "bind_address": { "type": [ "string", "null" ], "description": "Address and port the daemon should listen on, for server-polled mode." }, "credential_refs": { "type": [ "string", "null" ], "description": "Comma-separated credential/integration tokens (for the docker-compose env)." }, "heartbeat_interval": { "type": [ "integer", "null" ], "format": "int64", "description": "How often the daemon reports in, in seconds.", "minimum": 0 }, "interfaces": { "type": [ "string", "null" ], "description": "Comma-separated interface names." }, "log_file": { "type": [ "string", "null" ], "description": "Path the daemon should write its log file to." }, "log_level": { "type": [ "string", "null" ], "description": "Log verbosity the daemon should run at (e.g. `info`, `debug`)." }, "purpose": { "$ref": "#/components/schemas/InstallCommandType", "description": "`install` or `rekey` (with the api-key placeholder), or `reconfigure` (credential-free)." } } }, "InstallCommandType": { "type": "string", "description": "What the caller wants the command to do — the one axis that actually varies.\n\n`install` brings a new daemon up: it carries the api-key placeholder, fetches the binary, and\nspells out the connectivity + advanced config, and nothing else. `rekey` is the same command\nfor a daemon that already exists on a host (a legacy daemon getting its first bound key), so\nonce that daemon has connected it also names the install to act on. `reconfigure` adjusts an\nalready-installed daemon in place: no key, no fetch, just the server-held connectivity —\n`scanopy-daemon install` layers it over the existing `config.json`.\n\n`install` and `rekey` are separate because the record can't tell them apart: a daemon created\nin the wizard has `last_seen` set as soon as it connects, and the wizard refetches its command\nafter that, which must not change shape.", "enum": [ "install", "rekey", "reconfigure" ] }, "InstanceUsage": { "type": "object", "description": "Server instances an online key has checked in from within the last\n`LICENSE_SILENCE_THRESHOLD_DAYS`, and how many the organization's licence\ncovers (`None` when the plan has no cap), for production and\nnon-production servers. A server past its class's limit is refused an\nentitlement.", "required": [ "active", "non_production_active", "non_production_limit" ], "properties": { "active": { "type": "integer", "format": "int32", "description": "Production servers.", "minimum": 0 }, "limit": { "type": [ "integer", "null" ], "format": "int32", "description": "Production servers the key covers. `None` is no cap.", "minimum": 0 }, "non_production_active": { "type": "integer", "format": "int32", "description": "Servers started with `SCANOPY_NON_PRODUCTION`. Absent from clouds\nolder than the setting.", "minimum": 0 }, "non_production_limit": { "type": [ "integer", "null" ], "format": "int32", "description": "Non-production servers the key covers. `None` is no cap.", "minimum": 0 } } }, "IntegrationTarget": { "oneOf": [ { "type": "object", "title": "DaemonHost", "description": "The daemon's own host — realized as a 127.0.0.1 IP-override (e.g. a local Docker/Podman\nsocket, or any credential the user pins to the daemon host without naming its IP).", "required": [ "credential_id", "scope" ], "properties": { "credential_id": { "type": "string", "format": "uuid", "description": "Credential to use on the daemon host." }, "scope": { "type": "string", "enum": [ "DaemonHost" ] } } }, { "type": "object", "title": "Site", "description": "All hosts on the site, as a broadcast default credential. Daemons at or below\n`last_without_site_wire` call this scope `\"Network\"`; the wire rewrite in\n`server/shared/legacy.rs` translates it both ways.", "required": [ "credential_id", "scope" ], "properties": { "credential_id": { "type": "string", "format": "uuid", "description": "Credential to use across the site." }, "scope": { "type": "string", "enum": [ "Site" ] } } }, { "type": "object", "title": "Hosts", "description": "Specific host IPs — one IP-override per address.", "required": [ "credential_id", "ips", "scope" ], "properties": { "credential_id": { "type": "string", "format": "uuid", "description": "Credential to use on the listed addresses." }, "ips": { "type": "array", "items": { "type": "string" }, "description": "The host addresses this credential applies to." }, "scope": { "type": "string", "enum": [ "Hosts" ] } } } ], "description": "Per-daemon integration targeting, stored on the `Discovery` entity and delivered via the\ninit command at registration. Each entry references exactly one stored credential and says\nwhere it applies on this daemon. This is the single home for cred↔IP targeting — it replaces\nthe global, race-prone `credential.target_ips`.\n\nThe variants ARE the scopes; their strum [`Target`] discriminants are the capability enum that\n`CredentialType::targets()` returns and validates against (single source of truth). Every\ntarget carries a real `credential_id` — there is no credential-less branch and no nil\nsentinel; a local socket is just a credential whose type targets only the daemon host." }, "Interface": { "allOf": [ { "$ref": "#/components/schemas/InterfaceBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "display_name": { "type": [ "string", "null" ], "description": "What to call this interface when it has no `if_alias`/`if_descr`: the MAC it was\nidentified by, or \"Interface\" when it has neither.\n\nRead-only and computed from [`Interface::display_name`] — the same ladder topology port\nlabels an interface with, so it cannot be called one thing in a list and another on the\nmap. Set on every interface read from storage, so every endpoint that returns one carries\nit; absent on a daemon's own submission, and ignored on write.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ] }, "InterfaceBase": { "allOf": [ { "$ref": "#/components/schemas/MacEvidence", "description": "MAC address, usually from SNMP `ifPhysAddress`, with the evidence for it." }, { "$ref": "#/components/schemas/FdbMacs", "description": "Bridge FDB: learned MAC addresses on this switch port, with what read them.\nSingle-MAC ports can be resolved to neighbor links server-side.\nMulti-MAC ports indicate uplinks where LLDP/CDP is the better source." }, { "type": "object", "required": [ "host_id", "site_id" ], "properties": { "admin_status": { "oneOf": [ { "$ref": "#/components/schemas/IfAdminStatus", "description": "SNMP ifAdminStatus: 1=up, 2=down, 3=testing, or `None` where nothing read it.\n\nOptional rather than defaulting to `Up`: an advertisement carries no status, and recording\none as up would be a claim nothing made." }, { "type": "null" } ] }, "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "if_alias": { "type": [ "string", "null" ], "description": "SNMP ifAlias - user-configured description" }, "if_descr": { "type": [ "string", "null" ], "description": "SNMP ifDescr - interface description (e.g., GigabitEthernet0/1), where one was read.\n\n`None` for a source with nothing to put here — PROFINET DCP Identify carries no per-port\ndescription at all. Same principle as `if_index`/`if_type`: an absent value is `None`, never\na fabricated string standing in for it." }, "if_index": { "type": [ "integer", "null" ], "format": "int32", "description": "SNMP ifIndex — stable identifier within device, where one was read.\n\n`None` for a port learned from a neighbour's LLDP/CDP advertisement rather than from the\ndevice's own ifTable: the far end tells us the port exists and what it is called, never its\nindex. `0` used to stand in for that, which is a real ifIndex on some agents and made\n\"never read\" indistinguishable from \"read as zero\".\n\nNot the identity of the row. `match_existing_interface` tries `(host_id, if_name)` first\nand the live unique index is on that pair; the index tier only runs for a row that has one." }, "if_name": { "type": [ "string", "null" ], "description": "SNMP ifName - short interface name (e.g., Gi1/0/1)" }, "if_type": { "type": [ "integer", "null" ], "format": "int32", "description": "SNMP ifType - IANAifType integer (6=ethernet, 24=loopback, etc.), where one was read.\n\n`None` is *unknown*, never a type. Everything that filters on this must treat unknown as\nincluded — a port at the far end of a cable is physical by construction, and excluding it\nfor lack of a number would drop the row from resolution and from the map." }, "interface_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What reported this interface." }, "ip_address_id": { "type": [ "string", "null" ], "format": "uuid", "description": "FK to IPAddress entity - this port's IP assignment (must be on same host).\nOld daemons send this as \"interface_id\"." }, "ip_configured": { "type": "boolean", "description": "Whether the device's own SNMP `ipAddrTable` lists this ifIndex as carrying one of its\nconfigured IP addresses.\n\nIndependent of `ip_address_id`: that FK is set server-side and requires the interface's MAC\nto be unique on the host before it links anything (`plan_interface_ip_links`), so it stays\n`NULL` on exactly the hosts this field exists to help — a Windows NIC and its NDIS\nfilter/LWF pseudo-interfaces sharing one MAC (GH #668). `ipAddrTable` only ever binds an\naddress to a real IP-stack adapter; a filter driver is never a separate one, so this\ndistinguishes the physical interface among MAC-sharing candidates. `#[serde(default)]` so a\ndaemon predating this field is read as `false` on every row — never worse than today's\nbehavior." }, "native_vlan_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Native/untagged VLAN entity ID on this port (resolved from Q-BRIDGE dot1qPvid)" }, "neighbor_candidates": { "type": "array", "items": { "$ref": "#/components/schemas/InterfaceNeighborEvidence" }, "description": "Raw LLDP/CDP evidence this scan heard on the port — the wire shape a **current** daemon\nsubmits, one entry per distinct LLDP or CDP record. Drained into `interface_neighbor_\ncandidates` by the discovery ingest path (`InterfaceService::create_or_update_from_\ndiscovery`); never read anywhere else. Not `skip_serializing`: this field is the daemon's\n*outgoing* discovery payload as much as it is the server's read model, and that attribute\nhas no notion of direction — it silently dropped every daemon's submitted evidence before\nthe request ever left the process (found investigating the GH #701 candidate-persistence\nregression; nothing downstream of the wire was ever at fault). Harmless for API responses:\n`create_or_update_from_discovery` takes this field via `mem::take` before returning or\npersisting `Interface`, and it is not a stored column, so nothing read back from the\ndatabase or echoed in a response ever has it populated." }, "oper_status": { "oneOf": [ { "$ref": "#/components/schemas/IfOperStatus", "description": "SNMP ifOperStatus: 1=up, 2=down, 3=testing, 4=unknown, 5=dormant, 6=notPresent,\n7=lowerLayerDown — or `None` where nothing read it.\n\nDeliberately not `IfOperStatus::Unknown`, which is the MIB's value 4 and means *the device\nsaid it does not know*. \"We never asked\" is a different statement, and folding the two\nwould make an inferred port indistinguishable from one that reported unknown." }, { "type": "null" } ] }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "speed_bps": { "type": [ "integer", "null" ], "format": "int64", "description": "Interface speed from ifSpeed/ifHighSpeed in bits per second" }, "vlan_ids": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" }, "description": "Tagged VLAN entity IDs on this port (resolved from Q-BRIDGE dot1qVlanCurrentEgressPorts)" }, "vlan_membership_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What read `native_vlan_id` and `vlan_ids`. One source for both, because they are read\ntogether and kept or replaced together." } } } ] }, "InterfaceDataComplete": { "type": "object", "description": "Which groups of per-interface data the daemon read in full during a scan.\n\nEach group comes from its own SNMP walk, and a walk cut short by a timeout yields exactly the\nsame empty result as a device that genuinely has nothing to report. Without knowing which\nhappened, the server overwrote good data with NULL on every truncation — and for the neighbour\nfields that also dropped the row out of L2 resolution permanently, since the resolution filter\nrequires a chassis id or CDP device id to be present.\n\nEvery field defaults to `true`, so a daemon predating this behaves exactly as before: it\nreports everything as authoritative and the server overwrites.", "properties": { "cdp": { "type": "boolean", "description": "`cdp_device_id`, `cdp_port_id`, `cdp_platform`, `cdp_address`" }, "fdb": { "type": "boolean", "description": "`fdb_macs`" }, "lldp": { "type": "boolean", "description": "`lldp_chassis_id`, `lldp_port_id`, `lldp_sys_name`, `lldp_port_desc`, `lldp_mgmt_addr`,\n`lldp_sys_desc`" }, "vlan_membership": { "type": "boolean", "description": "`native_vlan_id`, `vlan_ids`" } } }, "InterfaceInput": { "type": "object", "description": "Input for manually creating or updating an interface entry.\nUsed in `UpdateHostRequest`, synced the same way as `ip_addresses`/`ports`/`services`:\na client-provided `id` that already exists on this host is updated, one that doesn't is\ncreated, and an existing row missing from the list is deleted.", "required": [ "id", "if_index", "if_descr" ], "properties": { "admin_status": { "oneOf": [ { "$ref": "#/components/schemas/IfAdminStatus", "description": "SNMP ifAdminStatus" }, { "type": "null" } ] }, "id": { "type": "string", "format": "uuid", "description": "Client-provided UUID for this interface." }, "if_alias": { "type": [ "string", "null" ], "description": "SNMP ifAlias - user-configured description" }, "if_descr": { "type": "string", "description": "SNMP ifDescr - interface description (e.g., GigabitEthernet0/1)" }, "if_index": { "type": "integer", "format": "int32", "description": "SNMP ifIndex - stable identifier within device" }, "if_type": { "type": [ "integer", "null" ], "format": "int32", "description": "SNMP ifType - IANAifType integer (6=ethernet, 24=loopback, etc.)" }, "ip_address_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Optional FK to Interface - links this SNMP port to its IP assignment" }, "mac_address": { "type": [ "string", "null" ], "description": "MAC address from SNMP ifPhysAddress", "example": "a4:bb:6d:12:34:56", "pattern": "^(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$" }, "oper_status": { "oneOf": [ { "$ref": "#/components/schemas/IfOperStatus", "description": "SNMP ifOperStatus" }, { "type": "null" } ] }, "speed_bps": { "type": [ "integer", "null" ], "format": "int64", "description": "Interface speed in bits per second" } } }, "InterfaceNeighborCandidate": { "type": "object", "required": [ "id", "created_at", "base" ], "properties": { "base": { "$ref": "#/components/schemas/InterfaceNeighborCandidateBase" }, "created_at": { "type": "string", "format": "date-time", "description": "When a scan last reported this evidence. Resolution reads it as the evidence's freshness,\nso a group a scan could not finish reading keeps its previous value." }, "id": { "type": "string", "format": "uuid", "description": "This candidate row's own id. Candidates are replaced wholesale on every scan, so the id\ndoes not survive from one scan to the next." } } }, "InterfaceNeighborCandidateBase": { "type": "object", "required": [ "site_id", "interface_id", "evidence" ], "properties": { "evidence": { "$ref": "#/components/schemas/InterfaceNeighborEvidence" }, "interface_id": { "type": "string", "format": "uuid", "description": "The local interface that heard this neighbour advertisement." }, "site_id": { "type": "string", "format": "uuid", "description": "The site the reporting interface belongs to." } } }, "InterfaceNeighborEvidence": { "type": "object", "description": "The raw identity one LLDP or CDP record advertised about a neighbour on one port.\n\nOne candidate is one record — an LLDP entry and a CDP entry for the same physical neighbour are\nnever merged into a single row, even when they name the same device. That is what lets the\nreciprocal resolution tier dedup by remote host rather than by candidate count (see\n`hosts/service/topology/reciprocal.rs`), and what lets `edge_builder.rs` read each adjacency's\nown evidence-derived protocol instead of an interface-level predicate.\n\nThis is both the wire shape a daemon submits (nested under\n`InterfaceBase::neighbor_candidates`) and `InterfaceNeighborCandidate`'s stored field set — see\n`InterfaceNeighborCandidateBase`, which pairs this with the `(site_id, interface_id)` the\nwire submission does not need to repeat per candidate.", "properties": { "cdp_address": { "type": [ "string", "null" ], "description": "Remote management IP from CDP (cdpCacheAddress). IPv4 or IPv6.", "example": "192.168.1.1" }, "cdp_device_id": { "type": [ "string", "null" ], "description": "Remote device ID from CDP (typically hostname, locally unique)" }, "cdp_platform": { "type": [ "string", "null" ], "description": "Remote platform from CDP (e.g., \"Cisco IOS\")" }, "cdp_port_id": { "type": [ "string", "null" ], "description": "Remote port ID from CDP" }, "evidence_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What heard this record: the integration that read the device's neighbor table, or lldpd on\nthe daemon's own host." }, "lldp_chassis_id": { "oneOf": [ { "$ref": "#/components/schemas/LldpChassisId", "description": "Remote chassis identifier from LLDP neighbor (globally/locally unique)" }, { "type": "null" } ] }, "lldp_mgmt_addr": { "type": [ "string", "null" ], "description": "Remote management IP from LLDP neighbor (lldpRemManAddr). IPv4 or IPv6.", "example": "192.168.1.1" }, "lldp_port_desc": { "type": [ "string", "null" ], "description": "Remote port description from LLDP neighbor (lldpRemPortDesc)" }, "lldp_port_id": { "oneOf": [ { "$ref": "#/components/schemas/LldpPortId", "description": "Remote port identifier from LLDP neighbor" }, { "type": "null" } ] }, "lldp_sys_desc": { "type": [ "string", "null" ], "description": "Remote system description from LLDP neighbor (lldpRemSysDesc) - platform info" }, "lldp_sys_name": { "type": [ "string", "null" ], "description": "Remote system name from LLDP neighbor (lldpRemSysName)" } } }, "InterfaceNeighborRow": { "type": "object", "description": "One row of the merged read model: an interface's adjacency to one neighbour, whichever of the\ntwo resolved tables it lives in. `TopologyContext` loads a `Vec` of these per site\n(`live_or_as_of`-pinned the same way `interfaces` is) instead of reading a scalar field —\n`InterfaceNeighborRow` is what every topology file downstream of resolution actually iterates.", "required": [ "id", "interface_id", "neighbor" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "The resolved row's own id (from whichever of the two tables it came from) — carried so a\nrow can be told apart from another row naming the same pair (not possible today, since the\nnatural key is unique per table, but keeps the type honest about which row backs it)." }, "interface_id": { "type": "string", "format": "uuid", "description": "The local interface this adjacency belongs to." }, "neighbor": { "$ref": "#/components/schemas/Neighbor" }, "neighbor_seen_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When a scan last saw the evidence behind this adjacency. Absent when no scan has recorded\na time for it." } } }, "InterfaceNeighbours": { "type": "object", "description": "One interface's resolved adjacencies plus the raw LLDP/CDP evidence behind them, for the\nhost editor's per-interface panel. Read on its own so that panel does not load the site's\nwhole topology bundle to show one port.", "required": [ "neighbours", "candidates" ], "properties": { "candidates": { "type": "array", "items": { "$ref": "#/components/schemas/InterfaceNeighborCandidate" }, "description": "Every distinct LLDP/CDP record heard on this interface, resolved or not." }, "neighbours": { "type": "array", "items": { "$ref": "#/components/schemas/InterfaceNeighborRow" }, "description": "Adjacencies resolved for this interface." } } }, "Invite": { "allOf": [ { "$ref": "#/components/schemas/InviteBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "InviteBase": { "type": "object", "required": [ "organization_id", "permissions", "site_ids", "url", "created_by", "expires_at", "send_to" ], "properties": { "created_by": { "type": "string", "format": "uuid", "description": "User who sent the invite." }, "expires_at": { "type": "string", "format": "date-time", "description": "When this record stops being valid." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "permissions": { "$ref": "#/components/schemas/UserOrgPermissions", "description": "Role the invited user gets on acceptance." }, "send_to": { "type": [ "string", "null" ], "description": "Optional email address to send the invite to" }, "site_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The sites this entity applies to." }, "url": { "type": "string", "description": "Link the recipient follows to accept the invite." } } }, "InvoiceBillingAddress": { "type": "object", "description": "Postal address of the billing entity, as Stripe's Address Element returns it.", "required": [ "line1", "city", "postal_code", "country" ], "properties": { "city": { "type": "string", "description": "City, town or locality." }, "country": { "type": "string", "description": "Two-letter ISO country code." }, "line1": { "type": "string", "description": "Street address, including number." }, "line2": { "type": [ "string", "null" ], "description": "Suite, unit or floor, when the address has one." }, "postal_code": { "type": "string", "description": "Postal or ZIP code." }, "state": { "type": [ "string", "null" ], "description": "State, province or region. Absent where the country has no such level." } } }, "InvoiceBillingDetails": { "type": "object", "description": "Who an invoice is addressed to and what the buyer's finance team matches\nit against.", "required": [ "entity_name", "billing_email", "address" ], "properties": { "address": { "$ref": "#/components/schemas/InvoiceBillingAddress" }, "billing_email": { "type": "string", "format": "email", "description": "Where Stripe emails invoices." }, "entity_name": { "type": "string", "description": "Legal name of the organization being invoiced." }, "po_number": { "type": [ "string", "null" ], "description": "Purchase order number printed on every invoice." }, "tax_id": { "oneOf": [ { "$ref": "#/components/schemas/InvoiceBillingTaxId" }, { "type": "null" } ] } } }, "InvoiceBillingMode": { "type": "string", "description": "What to do once the billing entity is recorded.", "enum": [ "send_invoice", "quote" ] }, "InvoiceBillingRequest": { "type": "object", "description": "Switch the organization to paying by invoice.", "required": [ "details", "mode" ], "properties": { "details": { "$ref": "#/components/schemas/InvoiceBillingDetails" }, "mode": { "$ref": "#/components/schemas/InvoiceBillingMode" }, "plan": { "oneOf": [ { "$ref": "#/components/schemas/BillingPlan", "description": "Plan to invoice for. Required only when the organization has no live\nsubscription (a returning customer with no trial left); otherwise the\ncurrent plan is used and this is ignored." }, { "type": "null" } ] } } }, "InvoiceBillingStatus": { "type": "object", "description": "Invoice billing state shown on the License tab.", "required": [ "bills_by_invoice" ], "properties": { "bills_by_invoice": { "type": "boolean", "description": "The subscription is billed by sent invoice." }, "open_invoice": { "oneOf": [ { "$ref": "#/components/schemas/InvoiceSummary" }, { "type": "null" } ] }, "pending_quote": { "oneOf": [ { "$ref": "#/components/schemas/PendingQuote" }, { "type": "null" } ] }, "po_number": { "type": [ "string", "null" ], "description": "Purchase order number printed on invoices." }, "written_off_invoice": { "oneOf": [ { "$ref": "#/components/schemas/InvoiceSummary", "description": "An invoice this customer defaulted on and we wrote off. While one\nstands, they cannot take payment terms again." }, { "type": "null" } ] } } }, "InvoiceBillingTaxId": { "type": "object", "description": "Tax ID of the billing entity, as Stripe's Tax ID Element returns it.", "required": [ "tax_id_type", "value" ], "properties": { "tax_id_type": { "type": "string", "description": "Stripe tax ID type, e.g. `eu_vat` or `us_ein`." }, "value": { "type": "string", "description": "The registration number itself, in the format its type expects." } } }, "InvoiceSummary": { "type": "object", "description": "An issued invoice that has not been paid.", "required": [ "amount_due_cents", "currency" ], "properties": { "amount_due_cents": { "type": "integer", "format": "int64", "description": "Outstanding balance, in cents." }, "currency": { "type": "string", "description": "Three-letter ISO currency code the invoice is denominated in." }, "due_date": { "type": [ "string", "null" ], "format": "date-time", "description": "When payment is due. Absent on an invoice Stripe collects automatically,\nwhich is charged rather than sent." }, "hosted_invoice_url": { "type": [ "string", "null" ], "description": "Stripe-hosted page where the invoice can be viewed and paid." }, "number": { "type": [ "string", "null" ], "description": "Invoice number printed on the PDF, which a remittance references. Absent\nuntil Stripe finalizes the invoice." } } }, "Ixy": { "type": "object", "required": [ "x", "y" ], "properties": { "x": { "type": "integer", "description": "Horizontal position, which may be negative." }, "y": { "type": "integer", "description": "Vertical position, which may be negative." } } }, "LegacyCapabilities": { "type": "object", "description": "Legacy inbound-only capabilities blob.\n\nPre-0.15 daemons report their interfaced subnets as bare `subnet_id`s in this\n`capabilities` object (they predate the `interfaced_subnets: Vec`\nheartbeat channel). It is deserialize-only: the server never stores it, never\nechoes it in `DaemonResponse`, and it has no `SqlValue` variant. Reported ids\nare routed into the `daemon_interfaced_subnets` junction (existence-filtered)\nso legacy daemons keep reporting interfaced subnets. ≥0.15 daemons send the\n`Vec` channel instead and leave this empty.", "required": [ "interfaced_subnet_ids" ], "properties": { "interfaced_subnet_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Subnets the daemon has an interface on, as reported by older daemons." } } }, "LicenseInstance": { "allOf": [ { "$ref": "#/components/schemas/LicenseInstanceBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When the cloud created the record.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Id of the cloud's record for this instance.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When the cloud last changed the record.", "readOnly": true } } } ], "description": "A server instance that has checked in with an organization's online key,\nas the cloud records it." }, "LicenseInstanceBase": { "type": "object", "required": [ "organization_id", "first_seen_at", "last_seen_at", "active_since" ], "properties": { "active_since": { "type": "string", "format": "date-time", "description": "When the instance last started counting in its class: its first\ncheck-in, its first after a silence, or a change of class. Slots go\nto active instances in this order, so a server coming back never\ndisplaces one that held the slot meanwhile." }, "first_seen_at": { "type": "string", "format": "date-time", "description": "First check-in." }, "instance_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The id the server's database holds. `None` on the one row shared by\nservers too old to send it." }, "last_seen_at": { "type": "string", "format": "date-time", "description": "Latest check-in." }, "last_seen_ip": { "type": [ "string", "null" ], "description": "Source address the cloud observed on the latest check-in. The server\nnever sends an address; behind NAT several servers share one.", "example": "203.0.113.7" }, "non_production": { "type": [ "boolean", "null" ], "description": "Whether the latest check-in came from a server started with\n`SCANOPY_NON_PRODUCTION`. Production and non-production servers take\nslots from separate limits. `None` when that check-in came from a\nserver older than the setting: it counts as production and is never\nrefused." }, "organization_id": { "type": "string", "format": "uuid", "description": "Organization whose online key the instance checks in with." }, "server_version": { "type": [ "string", "null" ], "description": "Version the latest check-in reported, as semver text." } } }, "LicenseInstanceSummary": { "type": "object", "required": [ "instance", "active", "refused" ], "properties": { "active": { "type": "boolean", "description": "Whether the instance counts against the limit: it checked in within\nthe last week." }, "instance": { "$ref": "#/components/schemas/LicenseInstance" }, "refused": { "type": "boolean", "description": "Whether the instance is past its class's limit and refused its\nentitlement, so it is read-only." } } }, "LicenseInstancesResponse": { "type": "object", "description": "The server instances an organization's online key is running on.", "required": [ "instances", "usage" ], "properties": { "instances": { "type": "array", "items": { "$ref": "#/components/schemas/LicenseInstanceSummary" }, "description": "Every instance that has checked in, most recent first, including ones\nsilent long enough to no longer count." }, "usage": { "$ref": "#/components/schemas/InstanceUsage", "description": "Instances counting against the limit now, and the limit." } } }, "LicenseKeyResponse": { "type": "object", "required": [ "key", "key_type" ], "properties": { "key": { "type": "string", "description": "Signed key to set as `SCANOPY_LICENSE_KEY` on a self-hosted server." }, "key_type": { "$ref": "#/components/schemas/LicenseKeyType", "description": "Which key this is. An organization has one issued at a time." } } }, "LicenseKeyType": { "type": "string", "description": "The two keys an org owner can copy for a self-hosted server. An\norganization has one of these issued at a time, stored on the org row.\n\nSerde keeps the API spelling (`\"Online\"` / `\"Offline\"`); strum supplies the\nsnake_case text the column is stored as, the way `PlanStatus` does.", "enum": [ "Online", "Offline" ] }, "LicenseStatusDiscriminants": { "type": "string", "description": "Runtime license state as reported by the public config endpoint.", "enum": [ "valid", "expired", "invalid", "pending" ] }, "LldpChassisId": { "oneOf": [ { "type": "object", "title": "ChassisComponent", "description": "Subtype 1: Chassis component (e.g., backplane serial number)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "ChassisComponent" ] }, "value": { "type": "string", "description": "Subtype 1: Chassis component (e.g., backplane serial number)" } } }, { "type": "object", "title": "InterfaceAlias", "description": "Subtype 2: Interface alias (ifAlias from IF-MIB)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "InterfaceAlias" ] }, "value": { "type": "string", "description": "Subtype 2: Interface alias (ifAlias from IF-MIB)" } } }, { "type": "object", "title": "PortComponent", "description": "Subtype 3: Port component (e.g., backplane port number)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "PortComponent" ] }, "value": { "type": "string", "description": "Subtype 3: Port component (e.g., backplane port number)" } } }, { "type": "object", "title": "MacAddress", "description": "Subtype 4: MAC address (most common)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "MacAddress" ] }, "value": { "type": "string", "description": "Subtype 4: MAC address (most common)" } } }, { "type": "object", "title": "NetworkAddress", "description": "Subtype 5: Network address (IP address stored as string)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "NetworkAddress" ] }, "value": { "type": "string", "description": "Subtype 5: Network address (IP address stored as string)" } } }, { "type": "object", "title": "InterfaceName", "description": "Subtype 6: Interface name (ifName from IF-MIB)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "InterfaceName" ] }, "value": { "type": "string", "description": "Subtype 6: Interface name (ifName from IF-MIB)" } } }, { "type": "object", "title": "LocallyAssigned", "description": "Subtype 7: Locally assigned (device-specific identifier)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "LocallyAssigned" ] }, "value": { "type": "string", "description": "Subtype 7: Locally assigned (device-specific identifier)" } } } ], "description": "LLDP Chassis ID subtypes per IEEE 802.1AB.\n\nThe chassis ID identifies the remote device. Different network equipment\nmay use different subtypes depending on configuration and capabilities." }, "LldpPortId": { "oneOf": [ { "type": "object", "title": "InterfaceAlias", "description": "Subtype 1: Interface alias (ifAlias from IF-MIB)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "InterfaceAlias" ] }, "value": { "type": "string", "description": "Subtype 1: Interface alias (ifAlias from IF-MIB)" } } }, { "type": "object", "title": "PortComponent", "description": "Subtype 2: Port component (e.g., backplane port number)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "PortComponent" ] }, "value": { "type": "string", "description": "Subtype 2: Port component (e.g., backplane port number)" } } }, { "type": "object", "title": "MacAddress", "description": "Subtype 3: MAC address", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "MacAddress" ] }, "value": { "type": "string", "description": "Subtype 3: MAC address" } } }, { "type": "object", "title": "NetworkAddress", "description": "Subtype 4: Network address (IP address stored as string)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "NetworkAddress" ] }, "value": { "type": "string", "description": "Subtype 4: Network address (IP address stored as string)" } } }, { "type": "object", "title": "InterfaceName", "description": "Subtype 5: Interface name (ifName from IF-MIB)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "InterfaceName" ] }, "value": { "type": "string", "description": "Subtype 5: Interface name (ifName from IF-MIB)" } } }, { "type": "object", "title": "AgentCircuitId", "description": "Subtype 6: Agent circuit ID (used by some providers)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "AgentCircuitId" ] }, "value": { "type": "string", "description": "Subtype 6: Agent circuit ID (used by some providers)" } } }, { "type": "object", "title": "LocallyAssigned", "description": "Subtype 7: Locally assigned (device-specific identifier)", "required": [ "value", "subtype" ], "properties": { "subtype": { "type": "string", "enum": [ "LocallyAssigned" ] }, "value": { "type": "string", "description": "Subtype 7: Locally assigned (device-specific identifier)" } } } ], "description": "LLDP Port ID subtypes per IEEE 802.1AB.\n\nThe port ID identifies the specific port on the remote device." }, "LoginRequest": { "type": "object", "description": "Login request from client", "required": [ "email", "password" ], "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the account to sign in to." }, "password": { "type": "string", "format": "password", "description": "The account password.", "writeOnly": true } } }, "MacEvidence": { "type": "object", "properties": { "mac_address": { "type": "string", "description": "MAC address, as discovered.", "examples": [ "a4:bb:6d:12:34:56" ], "pattern": "^(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$" }, "mac_address_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "MalformedNeighbourConsequence": { "type": "string", "description": "What discarding a device's malformed neighbour records cost it.\n\nA slot value rather than two codes per reason: losing every link and losing some of them is a\ndifference in severity, not in failure mode, and the metric asks about mode. Splitting it into\ncodes would double the enum to say something the operator reads in one clause.", "enum": [ "AllLinksLost", "SomeLinksLost" ] }, "MalformedNeighbours": { "type": "object", "description": "Neighbour records discarded for want of the identifier that matches the far end.", "required": [ "address", "group", "discarded", "kept", "consequence", "credential_id" ], "properties": { "address": { "type": "string", "description": "The device that reported the records." }, "consequence": { "$ref": "#/components/schemas/MalformedNeighbourConsequence" }, "credential_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The stored credential the device was read with. `None` from a daemon that predates it." }, "discarded": { "type": "integer", "format": "int32", "description": "Records thrown away for want of a usable identifier.", "minimum": 0 }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" }, "kept": { "type": "integer", "format": "int32", "description": "Records that survived, which is what decides whether this cost the device some of its\ntopology or all of it.", "minimum": 0 } } }, "MarkChannel": { "type": "string", "description": "Where on an element card a mark paints.", "enum": [ "StateFill", "Status" ] }, "MatchConfidence": { "type": "string", "enum": [ "NotApplicable", "Low", "Medium", "High", "Certain" ] }, "MatchDetails": { "type": "object", "required": [ "reason", "confidence" ], "properties": { "confidence": { "$ref": "#/components/schemas/MatchConfidence", "description": "How strong the match is." }, "reason": { "$ref": "#/components/schemas/MatchReason", "description": "Why the service was matched to this definition." } } }, "MatchReason": { "oneOf": [ { "type": "object", "title": "Reason", "required": [ "type", "data" ], "properties": { "data": { "type": "string", "description": "Why the service was matched." }, "type": { "type": "string", "enum": [ "reason" ] } } }, { "type": "object", "title": "Container", "required": [ "type", "data" ], "properties": { "data": { "type": "array", "items": false, "prefixItems": [ { "type": "string", "description": "The rule that grouped these reasons." }, { "type": "array", "items": { "$ref": "#/components/schemas/MatchReason" }, "description": "The reasons it grouped." } ], "description": "Tuple of [name: string, children: MatchReason[]]", "maxItems": 2, "minItems": 2 }, "type": { "type": "string", "enum": [ "container" ] } } } ], "description": "Match reason - either a simple reason string or a container with nested reasons" }, "Member": { "oneOf": [ { "type": "object", "title": "User", "required": [ "id", "data", "status", "auth_method", "type" ], "properties": { "auth_method": { "type": "string", "description": "How the user signs in: ``, `+password`, `password` or `none`.\nProvider names come from the server's OIDC config." }, "data": { "$ref": "#/components/schemas/User" }, "id": { "type": "string", "format": "uuid", "description": "The user's id." }, "status": { "$ref": "#/components/schemas/MemberStatus" }, "type": { "type": "string", "enum": [ "user" ] } } }, { "type": "object", "title": "Invite", "required": [ "id", "data", "status", "type" ], "properties": { "data": { "$ref": "#/components/schemas/Invite" }, "id": { "type": "string", "format": "uuid", "description": "The invite's id." }, "status": { "$ref": "#/components/schemas/MemberStatus" }, "type": { "type": "string", "enum": [ "invite" ] } } } ], "description": "One row of the members list: a user, or an invite still waiting to be accepted." }, "MemberOrderField": { "type": "string", "description": "Fields the members list can be ordered and grouped by. Every one is derived in Rust.", "enum": [ "created_at", "email", "invite_status", "permissions", "oidc_provider", "updated_at", "invited_by", "expires_at", "email_verified", "oidc_linked_at" ] }, "MemberStatus": { "type": "string", "description": "A member's place in the organization, as the status column shows it.", "enum": [ "pending", "you", "user" ] }, "MergeSubnetRequest": { "type": "object", "description": "Request body for merging a subnet into the range that contains it.", "required": [ "into" ], "properties": { "into": { "type": "string", "format": "uuid", "description": "The subnet to merge into. Must contain the range being merged." } } }, "MetadataFilter": { "type": "object", "description": "A declared metadata filter on an element entity inside a view.\n\n`FilterValue::id` is the **stable identifier** — it's what each entity\nemits via `HasFilterValues`, what the hide-set in request options stores,\nand what matches entities on toggle. Changing an `id` silently clears\nusers' persisted hide entries for that value; `label` is display-only\nand can change freely.", "required": [ "filter_type", "entities", "label", "values", "applies" ], "properties": { "applies": { "$ref": "#/components/schemas/FilterApplication", "description": "Where this filter runs. Deliberately not defaulted: a new filter has to state which side it\nbelongs on, and the compiler is a better place to force that decision than a review comment.\nSee `FilterApplication` for when `Server` is permissible." }, "entities": { "type": "array", "items": { "$ref": "#/components/schemas/EntityDiscriminants" }, "description": "The entities this filter reads and hides. Hovering a value rings every listed entity\ncarrying it; hiding one writes it into the hide-set under each listed entity." }, "filter_type": { "$ref": "#/components/schemas/MetadataFilterType", "description": "What the filter narrows by." }, "label": { "type": "string", "description": "User-facing sub-section label (e.g. \"By Category\", \"By Virtualization\")." }, "values": { "type": "array", "items": { "$ref": "#/components/schemas/FilterValue" }, "description": "The choices offered for this filter." } } }, "MetadataFilterType": { "type": "string", "description": "The type of metadata filter. One variant per conceptually-distinct filter\nacross the app — Category (on Service), Virtualization (on Host), and so\non. Kept narrow on purpose: adding a new filter means adding a variant\nhere + a `HasFilterValues` impl on the relevant entity.", "enum": [ "Category", "Virtualization", "LinkState", "Staleness", "OperStatus", "GuestState" ] }, "MetadataFixture": { "type": "string", "description": "A metadata fixture whose entries name an enum-valued field, so the UI shows the fixture's\nname for the raw value. Serializes to the fixture key.", "enum": [ "if_oper_statuses", "guest_states", "subnet_types", "entity_sources", "dependency_types", "discovery_types", "service_definitions", "discovery_protocols", "permissions", "credential_types" ] }, "MsiInstall": { "type": "object", "description": "The Windows MSI install method. The MSI itself is a static release asset the UI links to; only\nthe per-daemon pre-fill data is tenant-specific.", "required": [ "filename", "omitted_config_keys" ], "properties": { "filename": { "type": "string", "description": "Filename encoding this daemon's non-secret config. Save or rename the downloaded MSI to\nthis name to pre-fill the installer — parse-filename.js decodes it. The api key is never\nencoded. Renaming a signed MSI doesn't affect its signature." }, "omitted_config_keys": { "type": "array", "items": { "type": "string" }, "description": "Config keys that did not fit in `filename` (a filename is capped at 255 characters). Empty\nfor any ordinary config. The MSI falls back to its built-in defaults for these, so the UI\nshould tell the user to set them in the installer — the other methods carry the full config." } } }, "Neighbor": { "oneOf": [ { "type": "object", "title": "Interface", "description": "Full resolution - the specific remote port was identified", "required": [ "id", "type" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Full resolution - the specific remote port was identified" }, "type": { "type": "string", "enum": [ "Interface" ] } } }, { "type": "object", "title": "Host", "description": "Partial resolution - the remote device was identified but not the specific port", "required": [ "id", "type" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Partial resolution - the remote device was identified but not the specific port" }, "type": { "type": "string", "enum": [ "Host" ] } } } ], "description": "One resolved adjacency, tagged by which of the two tables it came from — the same shape\n`Neighbor` gave a single interface before GH #701, now one per row rather than at most one per\ninterface." }, "NetworkIdentityVirtualization": { "type": "object", "description": "An address and MAC that a host presents from an interface of its own beyond its configured\nNICs: a macvlan shim, a virtual IP, a service given its own LAN address, an emulated device.\nThe reporting source proves only that the interface lives inside that host, not what it is,\nso nothing here classifies it. The owner is the host's Network Identities service\n(`HostBase::virtualization_service_id`), and the interface it sits on is\n`HostBase::virtualization_interface_id`, both real columns with foreign keys. A stored row from\nbefore the interface became a column still carries an `interface` name here, which is ignored." }, "Node": { "allOf": [ { "$ref": "#/components/schemas/NodeType", "description": "Whether this node is a container or an element, and what it stands for." }, { "type": "object", "required": [ "id", "position", "size" ], "properties": { "header": { "type": [ "string", "null" ], "description": "Heading drawn at the top of a container node." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "position": { "$ref": "#/components/schemas/Ixy", "description": "Where the node sits in the layout." }, "size": { "$ref": "#/components/schemas/Uxy", "description": "Width and height of the node." } } } ] }, "NodeType": { "oneOf": [ { "type": "object", "title": "Container", "required": [ "node_type" ], "properties": { "associated_service_definition": { "type": [ "string", "null" ], "description": "Service definition ID for logo rendering (e.g. \"Docker\", \"Proxmox VE\").\nUsed by Hypervisor and Stack subcontainers to show the service's logo." }, "color": { "oneOf": [ { "$ref": "#/components/schemas/Color", "description": "Display color (set by graph builder from the source entity, e.g. subnet type)" }, { "type": "null" } ] }, "container_type": { "$ref": "#/components/schemas/ContainerType", "description": "What this container groups — a host, a subnet, an application, and so on." }, "element_rule_id": { "type": [ "string", "null" ], "format": "uuid", "description": "ID of the element rule that created this container (for subcontainers like NestedTag, Hypervisor, etc.)" }, "entity_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The entity this container represents (e.g. host ID for Host containers,\nsubnet ID for Subnet containers). Used for ownership mapping on the frontend." }, "icon": { "type": [ "string", "null" ], "description": "Display icon name (set by graph builder from the source entity, e.g. subnet type)" }, "node_type": { "type": "string", "enum": [ "Container" ] }, "parent_container_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Container this one nests inside, for subcontainers." }, "will_accept_edges": { "type": "boolean", "description": "When true, this container accepts edges with `will_target_container`, causing\nthem to visually attach here instead of at elements inside." } } }, { "allOf": [ { "$ref": "#/components/schemas/ElementEntityType" }, { "type": "object", "required": [ "host_id" ], "properties": { "container_id": { "type": "string", "format": "uuid", "description": "Container this element is drawn inside." }, "entity_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The entity this element stands for, of the type `element_type` names: the frontend's\none lookup from a card to what it describes. `None` only for an IP address element with\nno known address." }, "host_id": { "type": "string", "format": "uuid", "description": "Host the element belongs to." }, "inline_groups": { "type": "array", "items": { "$ref": "#/components/schemas/InlineGroup" }, "description": "Visual grouping metadata for services inlined on this element.\nPopulated by element rules (e.g., Docker containers on a VM host\nget InlineGroups with Header/Member roles for dotted-border rendering)." } } }, { "type": "object", "title": "Element", "required": [ "node_type" ], "properties": { "node_type": { "type": "string", "enum": [ "Element" ] } } } ] } ] }, "OidcProviderMetadata": { "type": "object", "required": [ "name", "slug" ], "properties": { "logo": { "type": [ "string", "null" ], "description": "Logo to show on the login button, when the provider has one configured." }, "name": { "type": "string", "description": "Display name of the identity provider, shown on the login button." }, "slug": { "type": "string", "description": "URL-safe identifier used in the provider's login and link endpoints." } } }, "OnboardingOperationDiscriminants": { "type": "string", "enum": [ "OrgCreated", "OnboardingModalCompleted", "PlanSelected", "DaemonPromptDismissed", "DaemonPromptAccepted", "FirstDaemonRegistered", "FirstTopologyRebuild", "FirstDiscoveryCompleted", "FirstHostDiscovered", "SecondSiteCreated", "FirstTagCreated", "FirstDependencyCreated", "FirstUserApiKeyCreated", "FirstInterfaceCredentialCreated", "FirstApplicationTagCreated", "FirstCredentialCreated", "FirstSnapshotCreated", "InviteSent", "InviteAccepted", "ProfileCompleted", "ReferralSourceCompleted" ] }, "OnboardingSiteState": { "type": "object", "description": "Site data in onboarding state response", "required": [ "name" ], "properties": { "id": { "type": [ "string", "null" ], "format": "uuid", "description": "Site ID (if created)" }, "name": { "type": "string", "description": "Site name" } } }, "OnboardingStateResponse": { "type": "object", "description": "Response from onboarding state endpoint", "properties": { "org_name": { "type": [ "string", "null" ], "description": "Organization name from pending setup" }, "site": { "oneOf": [ { "$ref": "#/components/schemas/OnboardingSiteState", "description": "Site from pending setup (with name and ID)" }, { "type": "null" } ] }, "site_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Site ID from pending setup (if any)" }, "step": { "type": [ "string", "null" ], "description": "Current onboarding step (if any)" }, "use_case": { "oneOf": [ { "$ref": "#/components/schemas/UseCase", "description": "Use case selection (homelab, company, msp)" }, { "type": "null" } ] } } }, "OnboardingStepRequest": { "type": "object", "description": "Request to save onboarding step", "required": [ "step" ], "properties": { "step": { "type": "string", "description": "Identifier of the onboarding step the user has reached." }, "use_case": { "oneOf": [ { "$ref": "#/components/schemas/UseCase", "description": "Use case selection (homelab, company, msp)" }, { "type": "null" } ] } } }, "OrderDirection": { "type": "string", "description": "Direction for ORDER BY clauses.", "enum": [ "asc", "desc" ] }, "Organization": { "allOf": [ { "$ref": "#/components/schemas/OrganizationBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "OrganizationBase": { "type": "object", "required": [ "name", "plan", "plan_status", "onboarding" ], "properties": { "air_gapped_key_current_until": { "type": [ "string", "null" ], "format": "date-time", "description": "The date this org's air-gapped key stays current until, or `None` when\nit holds an online key or that date has passed.\n\nComputed on read from `license_key_type` and `license_paid_through`,\nnever stored. It carries the one fact the UI needs, that the org cannot\nchange plan yet, without exposing `license_key_type`, which stays\ninternal. See [`Organization::air_gapped_key_current_until`].", "readOnly": true }, "bills_by_invoice": { "type": "boolean", "description": "Whether the subscription is billed by sent invoice, against a purchase\norder. Such an org has no card, so this is the other half of \"can this\norg pay?\" — see [`Organization::can_pay`].", "readOnly": true }, "discount_save_offer_active_until": { "type": [ "string", "null" ], "format": "date-time", "description": "When the currently-active save-offer discount window expires. The\nBillingTab chip renders only while `> now()`; expiry needs no\ncleanup job.", "readOnly": true }, "discount_save_offer_percent_off": { "type": [ "integer", "null" ], "format": "int64", "description": "Percent off the currently-active save-offer discount applies. Read\nlive by the BillingTab chip so a future coupon swap renders the new\nvalue without a code change.", "readOnly": true }, "has_payment_method": { "type": "boolean", "description": "Whether a payment method is on file.", "readOnly": true }, "last_discount_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Most recent save-offer-discount application. NULL = never. Drives the\nonce-per-org eligibility check in `apply_discount_save_offer` and\nhides the Discount panel on the cancel modal for any return visit.", "readOnly": true }, "last_downgrade_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Most recent downgrade event timestamp (paid→cheaper, or paid→cancelled);\npowers the 14-day downgrade banner.", "readOnly": true }, "last_downgrade_from_plan": { "oneOf": [ { "$ref": "#/components/schemas/BillingPlan", "description": "Plan downgraded from at `last_downgrade_at`; pairs with the timestamp\nso the banner can render \"you downgraded from Pro\"." }, { "type": "null" } ], "readOnly": true }, "last_paused_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Most recent `Paused` billing event's timestamp; powers the 6-month\nrolling pause cooldown.", "readOnly": true }, "license_checkin_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Last time a self-hosted server fetched an entitlement with this org's\nonline license key.", "readOnly": true }, "license_paid_through": { "type": [ "string", "null" ], "format": "date-time", "description": "When the org's self-hosted license is paid through: the trial end\nduring a self-hosted trial, then the end of the last paid invoice's\nservice period. While a sent invoice is unpaid, its due date plus a\ngrace window. License keys and entitlements expire 7 days later.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for this organization." }, "next_renewal_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Stripe `subscription.items.data[0].current_period_end`, mirrored on\nevery billing event that re-anchors the period (checkout, trial start\n/ end, plan change, renewal, pause/resume, reactivate). Cleared by\nSubscriptionCancelled. Powers the \"Next renewal on …\" line in\nBillingPlanModal; the UI interprets the value based on plan_status\n(hide for paused/cancelled/past_due where the stored value can be\nstale or meaningless).", "readOnly": true }, "onboarding": { "type": "array", "items": { "$ref": "#/components/schemas/OnboardingOperationDiscriminants", "readOnly": true }, "description": "Progress through first-run setup." }, "plan": { "oneOf": [ { "$ref": "#/components/schemas/BillingPlan", "description": "The plan this organization is on." }, { "type": "null" } ], "readOnly": true }, "plan_status": { "oneOf": [ { "$ref": "#/components/schemas/PlanStatus", "description": "Current billing state of that plan." }, { "type": "null" } ], "readOnly": true }, "trial_end_date": { "type": [ "string", "null" ], "format": "date-time", "description": "When the free trial ends, if one is running.", "readOnly": true }, "trial_extended_used": { "type": "boolean", "description": "Whether the org has used its one-time trial-extend perk.", "readOnly": true }, "use_case": { "$ref": "#/components/schemas/UseCase", "description": "Use case selection (homelab, company, msp, other)" } } }, "OsFamily": { "type": "string", "description": "The OS family of a machine a credential's paths live on. Paths, sockets and the shell a script\nruns in all follow from it.", "enum": [ "Unix", "Windows" ] }, "PaginatedApiMeta": { "type": "object", "description": "API metadata for paginated list responses (pagination is always present)", "required": [ "api_version", "server_version", "pagination" ], "properties": { "api_version": { "type": "integer", "format": "int32", "description": "API version (integer, increments on breaking changes)", "minimum": 0 }, "pagination": { "$ref": "#/components/schemas/PaginationMeta", "description": "Pagination info" }, "server_version": { "type": "string", "description": "Server version (semver)", "example": "0.19.0" } }, "example": { "api_version": 1, "pagination": { "has_more": true, "limit": 50, "offset": 0, "total_count": 142 }, "server_version": "0.19.0" } }, "PaginatedApiResponse_Credential": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/CredentialBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "is_default" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "is_default": { "type": "boolean", "description": "Whether this is the organization's seeded default for its type: identical to the\ncredential every organization starts with. A default can't be deleted, and its settings\ncan't change. Computed from `credential_type`, so a value sent in a request is ignored.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_DaemonResponse": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/DaemonBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "version_status", "interfaced_subnet_ids", "health" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "health": { "$ref": "#/components/schemas/DaemonHealth", "description": "The one status the daemon's chip shows, the most severe that applies." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "interfaced_subnet_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Subnets this daemon has interfaces on, loaded from the\n`daemon_interfaced_subnets` junction (replaces the old\n`capabilities.interfaced_subnet_ids` JSONB field)." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "version_status": { "$ref": "#/components/schemas/DaemonVersionStatus", "description": "Computed version status including health and warnings" } } } ], "description": "Daemon response for UI including computed version status" }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_Dependency": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/DependencyBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "color": "Blue", "created_at": "2026-01-15T10:30:00Z", "dependency_type": "RequestPath", "description": "HTTP/HTTPS services dependency", "edge_style": "Bezier", "id": "550e8400-e29b-41d4-a716-446655440008", "lineage_id": null, "members": { "service_ids": [], "type": "Services" }, "name": "Web Services", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_Discovery": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/DiscoveryBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "integration_targets" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "force_full_scan": { "type": "boolean", "description": "When true, the next scan will be a full port scan regardless of interval" }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "integration_targets": { "type": "array", "items": { "$ref": "#/components/schemas/IntegrationTarget" }, "description": "Per-daemon integration targeting: which integrations run on this daemon, and on which\nIPs. Delivered via the init command at registration and editable via the discovery\nmodal. This is the single home for cred↔IP targeting; it replaces the global\n`credential.target_ips` (race-prone, consumed once).\n\nOne-shot: a target is offered to the daemon until a scan completes successfully, then\ndropped by [`Discovery::apply_successful_scan`]. Credentials that earned a durable home\nduring the scan keep being retried from there — `host_credentials` for one that probed\nsuccessfully, `site_credentials` for a broadcast one (see\n[`Discovery::take_site_scope_credential_ids`])." }, "next_run_is_full_scan": { "type": "boolean", "description": "Whether the next run of this discovery will be a full scan of all 65,535 ports rather than\na light scan of common ports. Computed by the server on read and never stored.", "readOnly": true }, "scan_count": { "type": "integer", "format": "int32", "description": "Number of completed scans (incremented by server on session completion)", "readOnly": true, "minimum": 0 }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_HostResponse": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "type": "object", "description": "Response type for host endpoints.\nIncludes children (ip_addresses, ports, services, interfaces).", "required": [ "id", "created_at", "updated_at", "last_seen_at", "name", "site_id", "source", "hidden", "tags", "firmware_revision", "software_revision", "os", "ip_addresses", "ports", "services", "interfaces" ], "properties": { "asset_tag": { "type": [ "string", "null" ], "description": "The asset tag the device reports: ENTITY-MIB entPhysicalAssetID. Read-only: discovery is\nthe only writer, so it always says what the device itself carries.", "readOnly": true }, "asset_tag_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the asset tag. Read-only.", "readOnly": true }, "chassis_id": { "type": [ "string", "null" ], "description": "LLDP chassis identifier, used to match the host to its neighbours." }, "chassis_id_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the LLDP chassis identifier. Read-only: decided by whichever source read it.", "readOnly": true }, "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "credential_assignments": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialAssignment" }, "description": "Credentials assigned to scan this host." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the host." }, "device_type": { "$ref": "#/components/schemas/DeviceType", "description": "What this host is, as a device: the role its icon shows everywhere it appears. Computed\nby [`Host::device_type`] from its services, then its OS, as the topology computes it.\nRead-only.", "readOnly": true }, "display_name": { "type": [ "string", "null" ], "description": "What to call this host when `name` is empty: its hostname, sysName, chassis id or first\naddress, whichever it has. `None` when nothing identifies it.\n\nRead-only and separate from `name` rather than folded into it. `name` is what a person\ntyped and what the editor writes back, so filling it with a fallback would turn a chassis\nid into a name the next save persists. Computed from [`Host::display_name`], the same\nladder topology titles a host container with, so a host cannot be called one thing in the\nlist and another on the map.", "readOnly": true }, "display_name_rung": { "oneOf": [ { "$ref": "#/components/schemas/HostNameRung", "description": "Which rung of the ladder produced `display_name`. `None` exactly when `display_name` is.\n\nResolved from the same [`Host::name_ladder`] call as `display_name`, so the UI can say\nwhere a host's title came from without walking the rungs itself." }, { "type": "null" } ], "readOnly": true }, "firmware_revision": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalFirmwareRev — firmware revision of the device. Read-only, as above.", "readOnly": true }, "firmware_revision_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the firmware revision. Read-only: decided by whichever source read it.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery run that first observed this host. Drives the \"First found by\" column.", "readOnly": true }, "hidden": { "type": "boolean", "description": "Always `false`. Kept because daemons before v0.17.23 require it when they read a host\nback; a host is hidden from topology by a tag of the Visibility group.", "readOnly": true }, "hostname": { "type": [ "string", "null" ], "description": "Hostname as resolved or reported by the host." }, "hostname_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced `hostname`: a PTR lookup, the host's own OS, a controller, mDNS, or a person.\nRead-only: decided by whichever source read it.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/Interface" }, "description": "SNMP ifTable entries" }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "IP addresses on this host." }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery run that last observed this host. Drives the \"Last found by\" column.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "Last time discovery observed this host. User-facing (drives the \"Last\nseen\" column and the stale badge), which is why it is carried here while\nthe version-history columns are not." }, "management_url": { "type": [ "string", "null" ], "description": "Link to the host's own management interface." }, "management_url_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the management URL. Read-only: decided by whichever source read it.", "readOnly": true }, "manufacturer": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalMfgName — hardware manufacturer. Read-only, as above.", "readOnly": true }, "manufacturer_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the manufacturer. Read-only: decided by whichever source read it.", "readOnly": true }, "model": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalModelName — hardware model. Read-only, as above.", "readOnly": true }, "model_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the model. Read-only: decided by whichever source read it.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for the host." }, "name_ladder": { "type": "array", "items": { "$ref": "#/components/schemas/HostNameLadderEntry", "readOnly": true }, "description": "Every rung of the display-name ladder for this host, highest first, with what each holds." }, "name_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced `name`. Read-only: it is decided by whoever supplied the name, not by the\ncaller.", "readOnly": true }, "os": { "oneOf": [ { "$ref": "#/components/schemas/HostOs", "description": "The host's operating system. Read-only: written by whichever source read or matched it.\nLenient, so a daemon tolerates a family added on a newer server." }, { "type": "null" } ], "readOnly": true }, "os_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the OS. Read-only, as above. A source whose method is `Inferred` matched the OS\nin a string the host emitted rather than reading it off the host.", "readOnly": true }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/Port" }, "description": "Open ports on this host." }, "serial_number": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalSerialNum — hardware serial number. Read-only, as above.", "readOnly": true }, "serial_number_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the serial number. Read-only: decided by whichever source read it.", "readOnly": true }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Services running on this host." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "software_revision": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalSoftwareRev — software revision of the device. Read-only, as above.", "readOnly": true }, "software_revision_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced the software revision. Read-only: decided by whichever source read it.", "readOnly": true }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "How this host came to be known — discovered, imported, or created by hand." }, "sys_contact": { "type": [ "string", "null" ], "description": "SNMP sysContact — administrative contact as configured on the device." }, "sys_contact_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysContact. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_descr": { "type": [ "string", "null" ], "description": "SNMP sysDescr — the device's own description of itself." }, "sys_descr_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysDescr. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_location": { "type": [ "string", "null" ], "description": "SNMP sysLocation — physical location as configured on the device." }, "sys_location_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysLocation. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_name": { "type": [ "string", "null" ], "description": "SNMP sysName.0 — the administratively-assigned hostname. Read-only: discovery collects it\nfrom the device, so neither create nor update accepts it.", "readOnly": true }, "sys_name_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysName. Read-only: decided by whichever source read it.", "readOnly": true }, "sys_object_id": { "type": [ "string", "null" ], "description": "SNMP sysObjectID — the vendor's identifier for the device model." }, "sys_object_id_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What produced SNMP sysObjectID. Read-only: decided by whichever source read it.", "readOnly": true }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "virtualization_depth": { "type": "integer", "format": "int32", "description": "How many hosts sit above this one in its virtualization chain.", "readOnly": true, "minimum": 0 }, "virtualization_interface_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The interface on the virtualizing host that presents this host, for a network identity." }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." }, { "type": "null" } ] }, "virtualization_parent_host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The host this one runs under: the host of its virtualizing service.", "readOnly": true }, "virtualization_root_host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The host at the top of this host's virtualization chain, itself when it is the top.\n`null` when the host neither runs under nor runs another host.", "readOnly": true }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } }, "example": { "asset_tag": "IT-00412", "asset_tag_source": { "Probe": "Snmp" }, "chassis_id_source": "Unspecified", "created_at": "2026-01-15T10:30:00Z", "credential_assignments": [], "description": "Primary web server", "device_type": "Server", "display_name": "web-server-01", "display_name_rung": "Name", "firmware_revision": null, "firmware_revision_source": "Unspecified", "first_discovery_id": null, "hidden": false, "hostname": "web-server-01.local", "hostname_source": "Manual", "id": "550e8400-e29b-41d4-a716-446655440003", "interfaces": [ { "admin_status": "Up", "created_at": "2026-01-15T10:30:00Z", "display_name": "Uplink to Core Switch", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-44665544000f", "if_alias": "Uplink to Core Switch", "if_descr": "GigabitEthernet0/1", "if_index": 1, "if_name": "Gi0/1", "if_type": 6, "interface_source": { "Probe": "Snmp" }, "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "ip_configured": true, "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "mac_address_source": "ArpReply", "neighbor_candidates": [], "oper_status": "Up", "site_id": "550e8400-e29b-41d4-a716-446655440002", "speed_bps": 1000000000, "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "vlan_membership_source": "Unspecified" } ], "ip_addresses": [ { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440005", "ip_address": "192.168.1.100", "ip_address_source": "ArpReply", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "mac_address_source": "ArpReply", "name": "eth0", "position": 0, "site_id": "550e8400-e29b-41d4-a716-446655440002", "subnet_id": "550e8400-e29b-41d4-a716-446655440004", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "management_url_source": "Unspecified", "manufacturer_source": "Unspecified", "model_source": "Unspecified", "name": "web-server-01", "name_ladder": [ { "rung": "Name", "source": "Manual", "value": "web-server-01" }, { "rung": "Hostname", "source": "Manual", "value": "web-server-01.local" }, { "rung": "SysName", "source": null, "value": null }, { "rung": "ChassisId", "source": null, "value": null }, { "rung": "Address", "source": null, "value": "192.168.1.100" } ], "name_source": "Manual", "os": null, "os_source": "Unspecified", "ports": [ { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440006", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "number": 80, "port_source": "PortScan", "protocol": "Tcp", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "serial_number_source": "Unspecified", "services": [ { "bindings": [ { "created_at": "2026-10-22T23:24:30.781183Z", "first_discovery_id": null, "id": "3d1f60f4-37ad-4de1-a050-6ee562ea6097", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-10-22T23:24:30.781183Z", "lineage_id": null, "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Port", "updated_at": "2026-10-22T23:24:30.781183Z", "valid_from": "2026-10-22T23:24:30.781183Z", "valid_to": null } ], "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440007", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "nginx", "position": 0, "service_definition": "Consul", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_metadata": null, "virtualization_service_id": null } ], "site_id": "550e8400-e29b-41d4-a716-446655440002", "software_revision": null, "software_revision_source": "Unspecified", "source": { "type": "Manual" }, "sys_contact_source": "Unspecified", "sys_descr_source": "Unspecified", "sys_location_source": "Unspecified", "sys_name_source": "Unspecified", "sys_object_id_source": "Unspecified", "tags": [], "updated_at": "2026-01-15T10:30:00Z", "virtualization_depth": 0, "virtualization_interface_id": null, "virtualization_metadata": null, "virtualization_parent_host_id": null, "virtualization_root_host_id": null, "virtualization_service_id": null } }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_Member": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "oneOf": [ { "type": "object", "title": "User", "required": [ "id", "data", "status", "auth_method", "type" ], "properties": { "auth_method": { "type": "string", "description": "How the user signs in: ``, `+password`, `password` or `none`.\nProvider names come from the server's OIDC config." }, "data": { "$ref": "#/components/schemas/User" }, "id": { "type": "string", "format": "uuid", "description": "The user's id." }, "status": { "$ref": "#/components/schemas/MemberStatus" }, "type": { "type": "string", "enum": [ "user" ] } } }, { "type": "object", "title": "Invite", "required": [ "id", "data", "status", "type" ], "properties": { "data": { "$ref": "#/components/schemas/Invite" }, "id": { "type": "string", "format": "uuid", "description": "The invite's id." }, "status": { "$ref": "#/components/schemas/MemberStatus" }, "type": { "type": "string", "enum": [ "invite" ] } } } ], "description": "One row of the members list: a user, or an invite still waiting to be accepted." }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_Service": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/ServiceBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "bindings": [ { "created_at": "2026-10-22T23:24:30.826318Z", "first_discovery_id": null, "id": "c19b1aeb-2d41-485c-9676-de0408fc1319", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-10-22T23:24:30.826318Z", "lineage_id": null, "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Port", "updated_at": "2026-10-22T23:24:30.826318Z", "valid_from": "2026-10-22T23:24:30.826318Z", "valid_to": null } ], "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440007", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "nginx", "position": 0, "service_definition": "Consul", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_metadata": null, "virtualization_service_id": null } }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_SubnetResponse": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/Subnet" }, { "type": "object", "required": [ "used_addresses", "usable_addresses", "nesting_depth", "nesting_root_id", "range_evidence" ], "properties": { "nesting_depth": { "type": "integer", "format": "int32", "description": "How many ranges this one sits inside: 0 for a range nested in nothing.", "readOnly": true, "minimum": 0 }, "nesting_root_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The outermost range of the tree this one sits in: itself when it holds others and sits in\nnone. `null` for a range that nests with nothing.", "readOnly": true }, "parent_subnet_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most specific other subnet on the same site whose range contains this one. `null` when\nnone does. Children are the subnets whose parent is this one.", "readOnly": true }, "range_evidence": { "oneOf": [ { "$ref": "#/components/schemas/RangeEvidence", "description": "For a range Scanopy assumed from what LLDP/CDP neighbours published, the far ends it was\ndrawn around and the devices that saw them. `null` for every other range, and for an\nassumed one no scan created." }, { "type": "null" } ], "readOnly": true }, "usable_addresses": { "type": "integer", "format": "int64", "description": "Addresses a host can hold in this range. Saturates for IPv6 ranges wider than a `/64`.", "readOnly": true, "minimum": 0 }, "used_addresses": { "type": "integer", "format": "int64", "description": "Distinct addresses held in this range, including those filed in subnets nested inside it.", "readOnly": true, "minimum": 0 } } } ], "description": "A subnet together with what is derived from the rest of its site." }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_Tag": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/TagBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "color": "Green", "created_at": "2026-01-15T10:30:00Z", "description": "Production environment resources", "icon": null, "id": "550e8400-e29b-41d4-a716-44665544000a", "lineage_id": null, "name": "production", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "position": 1, "tag_group_id": "550e8400-e29b-41d4-a716-446655440010", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_Topology": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/TopologyBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_User": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/UserBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_UserApiKey": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/UserApiKeyBase" }, { "type": "object", "required": [ "id", "updated_at", "created_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginatedApiResponse_Vlan": { "type": "object", "description": "Response type for paginated list endpoints (pagination is always present in meta)", "required": [ "success", "data", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/VlanBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ] }, "description": "The page of results. Empty when nothing matched the query." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/PaginatedApiMeta", "description": "API and server version metadata, plus pagination counters." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "PaginationMeta": { "type": "object", "description": "Pagination metadata returned with paginated responses.", "required": [ "total_count", "limit", "offset", "has_more" ], "properties": { "group_counts": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/GroupCount" }, "description": "Size of every group, in the same order the rows are grouped, when the\nrequest specified a `group_by`. Lets a paginated client show a group's\ntrue size instead of the slice of it that happens to be on this page.\nAbsent when the list isn't grouped." }, "has_more": { "type": "boolean", "description": "Whether there are more items after this page" }, "limit": { "type": "integer", "format": "int32", "description": "Maximum items per page (as requested)", "minimum": 0 }, "offset": { "type": "integer", "format": "int32", "description": "Number of items skipped", "minimum": 0 }, "total_count": { "type": "integer", "format": "int64", "description": "Total number of items matching the filter (ignoring pagination)", "minimum": 0 } }, "example": { "has_more": true, "limit": 50, "offset": 0, "total_count": 142 } }, "PaginationParams": { "type": "object", "description": "Pagination parameters that can be composed into filter queries.\n\nDefault behavior:\n- `limit`: 50 (returns up to 50 results)\n- `offset`: 0 (starts from the beginning)\n- `limit=0`: No limit (returns all results)\n- `limit` values above 1000 are capped to 1000", "properties": { "limit": { "type": [ "integer", "null" ], "format": "int32", "description": "Maximum number of results to return (1-1000, default: 50). Use 0 for no limit.", "minimum": 0 }, "offset": { "type": [ "integer", "null" ], "format": "int32", "description": "Number of results to skip. Default: 0.", "minimum": 0 } } }, "PauseDuration": { "type": "string", "description": "Pause subscription duration. The cancel modal's `RadioGroup` posts\none of these enum variants verbatim — no integer parsing at the API\nboundary, the type is the contract.", "enum": [ "days30", "days60", "days90" ] }, "PauseSubscriptionRequest": { "type": "object", "required": [ "duration_days" ], "properties": { "duration_days": { "$ref": "#/components/schemas/PauseDuration", "description": "How long to pause billing for, in days." } } }, "PendingQuote": { "type": "object", "description": "An open quote waiting for the buyer's purchase order.", "required": [ "amount_total_cents", "currency", "expires_at" ], "properties": { "amount_total_cents": { "type": "integer", "format": "int64", "description": "Total per annual term, in cents." }, "currency": { "type": "string", "description": "Three-letter ISO currency code the quote is denominated in." }, "expires_at": { "type": "string", "format": "date-time", "description": "When the quote stops being valid, after which the buyer needs a new one." }, "number": { "type": [ "string", "null" ], "description": "Quote number printed on the PDF, which the purchase order references." } } }, "PlanConfig": { "type": "object", "required": [ "base_cents", "rate", "trial_days" ], "properties": { "base_cents": { "type": "integer", "format": "int64", "description": "Fixed charge per billing period, in cents." }, "host_cents": { "type": [ "integer", "null" ], "format": "int64", "description": "Charge per host beyond `included_hosts`, in cents." }, "included_hosts": { "type": [ "integer", "null" ], "format": "int64", "description": "Hosts included before per-host charges apply.", "minimum": 0 }, "included_orgs": { "type": [ "integer", "null" ], "format": "int64", "description": "Organizations allowed on one self-hosted server instance. `None` =\nunlimited. Only enforced for self-hosted deployments (see\n`provision_user`); cloud stays multi-tenant regardless. Defaulted so\nexisting stored plan JSON deserializes unchanged.", "minimum": 0 }, "included_seats": { "type": [ "integer", "null" ], "format": "int64", "description": "Seats included before per-seat charges apply.", "minimum": 0 }, "included_sites": { "type": [ "integer", "null" ], "format": "int64", "description": "Sites included before per-site charges apply.", "minimum": 0 }, "rate": { "$ref": "#/components/schemas/BillingRate", "description": "Billing interval this configuration is priced for." }, "seat_cents": { "type": [ "integer", "null" ], "format": "int64", "description": "Charge per seat beyond `included_seats`, in cents." }, "site_cents": { "type": [ "integer", "null" ], "format": "int64", "description": "Charge per site beyond `included_sites`, in cents.\nPlans stored before the site rename (org rows, Stripe subscription metadata) say\n`network_cents`." }, "trial_days": { "type": "integer", "format": "int32", "description": "Length of the free trial, in days. Zero when the plan has no trial.", "minimum": 0 } } }, "PlanStatus": { "type": "string", "description": "Derived subscription status — our domain enum, never Stripe's raw status.\nStripe webhook events map to typed `BillingOperation` variants at reception\n(in `billing/service.rs`); each variant deterministically implies a\n`PlanStatus` for downstream feature gates via\n`BillingOperation::implied_status`.\n\n`FromStr` is derived (via strum) so the storage layer can round-trip a\nsnake_case `text` column back into the typed value; `ToSchema` exposes\nthe enum as a stricter string union in the generated OpenAPI schema so\nthe frontend's `org.plan_status === 'paused'` comparisons are\ncompile-checked against the canonical variant list.", "enum": [ "active", "trialing", "past_due", "paused", "pending_cancellation", "cancelled" ] }, "PlanUsage": { "type": "object", "description": "Plan usage limits and current counts", "required": [ "host_count", "site_count", "seat_count" ], "properties": { "host_count": { "type": "integer", "format": "int64", "description": "Hosts currently counted against the plan.", "minimum": 0 }, "host_limit": { "type": [ "integer", "null" ], "format": "int64", "description": "Hosts included in the current plan. `null` when unlimited.", "minimum": 0 }, "seat_count": { "type": "integer", "format": "int64", "description": "Seats currently in use.", "minimum": 0 }, "seat_limit": { "type": [ "integer", "null" ], "format": "int64", "description": "Seats included in the current plan. `null` when unlimited.", "minimum": 0 }, "site_count": { "type": "integer", "format": "int64", "description": "Sites currently counted against the plan.", "minimum": 0 }, "site_limit": { "type": [ "integer", "null" ], "format": "int64", "description": "Sites included in the current plan. `null` when unlimited.", "minimum": 0 } } }, "PodmanVirtualization": { "type": "object", "properties": { "compose_project": { "type": [ "string", "null" ], "description": "Compose project the container belongs to, when it was started by Compose." }, "container_id": { "type": [ "string", "null" ], "description": "Podman container ID." }, "container_name": { "type": [ "string", "null" ], "description": "Container name as reported by Podman." }, "state": { "$ref": "#/components/schemas/GuestState", "description": "Whether the container was running at the last scan." } } }, "Port": { "allOf": [ { "$ref": "#/components/schemas/PortBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "description": "Port entity with custom serialization that flattens PortType fields.", "example": { "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440006", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "number": 80, "port_source": "PortScan", "protocol": "Tcp", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "PortBase": { "allOf": [ { "$ref": "#/components/schemas/PortType", "description": "Port number, transport protocol, and the well-known service they identify." }, { "type": "object", "required": [ "host_id", "site_id" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "port_source": { "$ref": "#/components/schemas/AttributeSource", "description": "What found this port open. Not part of the port's identity: equality and hashing compare\n`port_type` alone." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." } } } ], "description": "The base data for a Port entity (everything except id, created_at, updated_at)" }, "PortCoverage": { "type": "array", "items": { "$ref": "#/components/schemas/CoveredPorts" }, "description": "Every (address, protocol, port) a scan probed and got a definite answer from: open, refused,\nor silent after every retry. A probe that failed on the scanner's side (file descriptors\nexhausted, the scan cancelled) is left out, since it learned nothing about the port." }, "PortInput": { "type": "object", "description": "Input for creating or updating a port.\nUsed in both CreateHostRequest and UpdateHostRequest.\nClient must provide a UUID for the port.", "required": [ "id", "number", "protocol" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Client-provided UUID for this port" }, "number": { "type": "integer", "format": "int32", "description": "Port number (1-65535)", "minimum": 0 }, "protocol": { "$ref": "#/components/schemas/TransportProtocol", "description": "Transport protocol (Tcp or Udp)" } } }, "PortRange": { "type": "object", "description": "An inclusive run of port numbers.", "required": [ "start", "end" ], "properties": { "end": { "type": "integer", "format": "int32", "description": "Last port in the run, inclusive.", "minimum": 0 }, "start": { "type": "integer", "format": "int32", "description": "First port in the run.", "minimum": 0 } } }, "PortType": { "type": "object", "description": "Port type with number, protocol, and optional type identifier", "required": [ "number", "protocol" ], "properties": { "number": { "type": "integer", "description": "TCP or UDP port number", "maximum": 65535, "minimum": 0 }, "protocol": { "type": "string", "description": "Transport protocol the port is open on.", "enum": [ "Udp", "Tcp" ] }, "type": { "type": "string", "description": "Well-known port identifier. Auto-derived from number+protocol, so it is optional on create.", "enum": [ "Ssh", "Telnet", "DnsUdp", "DnsTcp", "Samba", "Nfs", "Ftp", "Ipp", "LdpTcp", "LdpUdp", "Ldap", "Ldaps", "Kerberos", "Snmp", "SnmpAlt", "Rdp", "Ntp", "Sip", "SipTls", "Rtsp", "Dhcp", "Http", "MySql", "PostgreSQL", "MongoDB", "Redis", "MsSql", "Docker", "DockerTls", "Kubernetes", "RabbitMqMgmt", "Cassandra", "Elasticsearch", "InfluxDb", "CouchDb", "Kafka", "Http3000", "Http5000", "Http8080", "Http8081", "Http8082", "Http8888", "Http9000", "Https", "Https8443", "Https9443", "Https10443", "Mqtt", "MqttTls", "AMQP", "AMQPTls", "Wireguard", "OpenVPN", "BACnet", "ModbusTcp", "OpcUa", "EtherNetIp", "JetDirect", "Custom" ] } } }, "ProfileUpdateRequest": { "type": "object", "description": "Request to update user profile (deferred marketing fields)", "properties": { "company_size": { "type": [ "string", "null" ], "description": "Company size bracket, collected during onboarding." }, "job_title": { "type": [ "string", "null" ], "description": "The user's job title, collected during onboarding." } } }, "ProvisionDaemonRequest": { "type": "object", "description": "Request to pre-provision a daemon (either mode) before it is installed.\nThis creates the daemon record + its 1:1 API key on the server so the install\ncommand shrinks to two flags.", "properties": { "daemon_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Mint a fresh 1:1 key for this existing daemon instead of creating a new record,\nkeeping its host, discovery jobs and history. Used to give a legacy daemon (no bound\nkey) a dedicated one. When set, `name`/`site_id`/`mode`/`url` are ignored — those\ncome from the existing record.\n\nOnly accepted for a daemon that has never checked in or has no bound key; a live\nprovisioned daemon is refused, since it has no way to learn the new key.\n\nNote: install commands are not generated here — call the install-command endpoint,\nwhich builds them idempotently and fills in the key this response returns." }, "mode": { "$ref": "#/components/schemas/DaemonMode", "description": "How the daemon communicates with the server. Defaults to DaemonPoll\n(the daemon dials out) for forward-compat with older clients." }, "name": { "type": [ "string", "null" ], "description": "Human-readable name for the daemon. Required unless `daemon_id` is set, in which case\nthe existing record's name is kept." }, "os": { "oneOf": [ { "$ref": "#/components/schemas/DaemonOs", "description": "The operating system picked in the create-daemon modal. Recorded on the daemon so a\ncredential set up for another OS's file paths can be refused for it. Ignored when\n`daemon_id` is set." }, { "type": "null" } ] }, "seed_credential_refs": { "type": "array", "items": { "$ref": "#/components/schemas/IntegrationTarget" }, "description": "Credential/integration references to seed onto the daemon's first\ndiscovery run. References only — never secret material. Empty by default." }, "site_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Site this daemon will be associated with. Required unless `daemon_id` is set, in\nwhich case the existing record's site is kept." }, "url": { "type": [ "string", "null" ], "description": "Reachable URL where the *server* can dial the daemon. Required for\nServerPoll, unused for DaemonPoll (the daemon dials out instead)." } } }, "ProvisionDaemonResponse": { "type": "object", "description": "Response from provisioning a daemon.\nContains the daemon record and the API key (shown only once).\n\nInstall commands are deliberately not here — fetch them from the install-command endpoint,\nwhich builds them idempotently and fills in this key. That keeps a display-only regenerate\n(advanced-setting change, OS switch) from re-minting the key.", "required": [ "daemon", "daemon_api_key" ], "properties": { "daemon": { "$ref": "#/components/schemas/DaemonResponse", "description": "The created daemon record (with version status)." }, "daemon_api_key": { "type": "string", "format": "password", "description": "The API key (plaintext) for daemon authentication.\nThis is shown only once - store it securely.", "readOnly": true } } }, "ProvisionEligibility": { "type": "object", "description": "Whether provisioning another daemon would be refused for the caller's organization.", "required": [ "email_verification_required" ], "properties": { "email_verification_required": { "type": "boolean", "description": "The organization already has a daemon and no owner has verified their email, so a new\ndaemon is refused until one does." } } }, "ProvisionalSubnet": { "type": "object", "description": "A range inferred from where unplaceable far ends say they live.\n\nCarries the evidence rather than just the verdict, because \"we invented a subnet\" is not a\nsentence an operator can act on: which of their devices saw it, on which ports, and what those\nfar ends call themselves is what lets them recognise a segment as real, correct the range, or\nsee that it is a device with a factory-default address.", "required": [ "cidr", "subnet_id", "addresses", "sys_names", "seen_by_host_ids", "widened_by_vlan" ], "properties": { "addresses": { "type": "array", "items": { "type": "string" }, "description": "The addresses that motivated it, in the order observed." }, "cidr": { "type": "string", "description": "The range, in CIDR notation." }, "seen_by_host_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The local devices that saw them." }, "subnet_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The subnet row created for it, so the UI can link straight to it. `None` once that subnet\nhas been deleted." }, "sys_names": { "type": "array", "items": { "type": "string" }, "description": "The far ends' own `sysName`s, where they sent any — the labels an operator recognises." }, "widened_by_vlan": { "type": "boolean", "description": "Whether a shared VLAN widened the range past the conventional prefix, rather than it being\nthe `/24` (or `/64`) a lone address is assumed to sit in. The difference is how much the\nrange rests on evidence versus on convention." } } }, "ProxmoxGuestType": { "type": "string", "description": "The two kinds of guest a Proxmox VE node runs. Both are hosts with their own addresses, so\nboth hang off the node's Proxmox VE service the same way.", "enum": [ "Qemu", "Lxc" ] }, "ProxmoxVirtualization": { "type": "object", "properties": { "guest_type": { "oneOf": [ { "$ref": "#/components/schemas/ProxmoxGuestType", "description": "Whether the guest is a QEMU virtual machine or an LXC container. `None` when the guest was\nassigned by hand or recorded before the Proxmox integration reported it." }, { "type": "null" } ] }, "state": { "$ref": "#/components/schemas/GuestState", "description": "Whether the guest was running at the last scan." }, "vm_id": { "type": [ "string", "null" ], "description": "Proxmox VMID of the guest." }, "vm_name": { "type": [ "string", "null" ], "description": "Guest name as configured in Proxmox." } } }, "PublicConfigResponse": { "type": "object", "required": [ "server_port", "disable_registration", "disable_password_login", "oidc_providers", "discount_save_offer_available", "has_integrated_daemon", "has_email_service", "public_url", "needs_cookie_consent", "deployment_type", "non_production", "license_in_grace_period", "org_limit_reached", "server_admin_contact_email", "license_signing_available", "license_key_buffer_days", "license_key_grace_days" ], "properties": { "available_upgrade": { "oneOf": [ { "$ref": "#/components/schemas/AvailableUpgrade", "description": "The newest promoted release, when it is newer than this server. `None`\non cloud, with `SCANOPY_DISABLE_UPDATE_CHECK` set, before the first\ncheck answers, or when this server is current." }, { "type": "null" } ] }, "deployment_type": { "$ref": "#/components/schemas/DeploymentType", "description": "How this instance is run: cloud, commercial self-hosted, or community." }, "disable_password_login": { "type": "boolean", "description": "Whether email/password login is turned off, leaving OIDC as the only method." }, "disable_registration": { "type": "boolean", "description": "Whether self-service sign-up is turned off on this deployment." }, "discount_save_offer_available": { "type": "boolean", "description": "`STRIPE_SAVE_OFFER_COUPON_ID` env var is set. When false, the\ncancel modal hides the discount save-offer panel so the user\ndoesn't see an option the deployment can't fulfil." }, "has_email_service": { "type": "boolean", "description": "Whether outbound email is configured. Invites and password resets need it." }, "has_integrated_daemon": { "type": "boolean", "description": "Whether a daemon runs alongside the server, so no separate install is needed to start scanning." }, "license_entitlement_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When Scanopy Cloud last answered this instance's license check-in.\nOnline keys only." }, "license_expiry": { "type": [ "string", "null" ], "format": "date", "description": "Hard expiry — the drop-dead date after which the server rejects\nthe key. Referenced by the grace-period banner." }, "license_in_grace_period": { "type": "boolean", "description": "True when the license is past `intended_exp` but not yet past\nthe hard `exp` — the silent grace window." }, "license_instances": { "oneOf": [ { "$ref": "#/components/schemas/InstanceUsage", "description": "How many server instances this instance's online key is running on,\nagainst how many it covers, as Scanopy Cloud reported at the last\ncheck-in. Online keys only." }, { "type": "null" } ] }, "license_intended_expiry": { "type": [ "string", "null" ], "format": "date", "description": "User-visible expiry — the date displayed to end users under\nnormal operation. 7 days earlier than `license_expiry` for keys\nissued after grace-period support landed." }, "license_key_buffer_days": { "type": "integer", "format": "int32", "description": "Days past an organization's paid-through date before a license key\nreaches its user-visible expiry. Published so the UI shows the same\ndates the mint path bakes into keys, instead of its own copy.", "minimum": 0 }, "license_key_grace_days": { "type": "integer", "format": "int32", "description": "Further days past the user-visible expiry before a key stops working.", "minimum": 0 }, "license_key_type": { "oneOf": [ { "$ref": "#/components/schemas/LicenseKeyType", "description": "Whether the configured key is an offline key or an online key that\nfetches its entitlement from Scanopy Cloud. `None` when no key applies." }, { "type": "null" } ] }, "license_signing_available": { "type": "boolean", "description": "Whether this deployment can sign license keys. False on any server\nwithout a signing key, where the Settings License tab and the\nself-hosted plans would otherwise offer something the mint path\nrefuses. Reads the built issuer rather than the config value, since a\nkey can be present but unparseable." }, "license_status": { "oneOf": [ { "$ref": "#/components/schemas/LicenseStatusDiscriminants", "description": "Runtime state of the configured license key. `None` on deployments\nthat don't require one (community and cloud)." }, { "type": "null" } ] }, "license_valid_through": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the paid period, shown as \"Valid through\" and in the expiry\nbanners. Falls back to the intended expiry for keys minted without a\npaid-through date." }, "needs_cookie_consent": { "type": "boolean", "description": "Whether the client should show a cookie-consent prompt." }, "non_production": { "type": "boolean", "description": "Whether this server was started with `SCANOPY_NON_PRODUCTION`." }, "oidc_providers": { "type": "array", "items": { "$ref": "#/components/schemas/OidcProviderMetadata" }, "description": "Identity providers available on the login screen." }, "org_limit_reached": { "type": "boolean", "description": "True when this self-hosted instance has reached its licensed\norganization cap (`included_orgs`), so new-org registration is blocked.\nAlways false on cloud (multi-tenant) and on unlimited-org plans." }, "posthog_key": { "type": [ "string", "null" ], "description": "Public analytics key, when analytics is enabled." }, "public_url": { "type": "string", "format": "uri", "description": "Base URL this server is reachable at, as configured by the operator." }, "server_admin_contact_email": { "type": "string", "format": "email", "description": "Admin contact email to show users blocked by `org_limit_reached`,\nfrom `SCANOPY_SERVER_ADMIN_CONTACT_EMAIL`." }, "server_port": { "type": "integer", "format": "int32", "description": "Port this server listens on.", "minimum": 0 }, "snapshot_retention_days_override": { "type": [ "integer", "null" ], "format": "int32", "description": "`SCANOPY_SNAPSHOT_RETENTION_DAYS_OVERRIDE` if set on this instance.\nFrontend uses it inside the plan-comparison view to display the\neffective retention for this deployment rather than the per-plan\nfixture default.", "minimum": 0 }, "stripe_publishable_key": { "type": [ "string", "null" ], "description": "Stripe publishable key, exposed so the frontend can mount Stripe\nElements (Payment Element) for in-app card collection. `None` when\nbilling isn't configured. Publishable keys are safe to expose to the\nbrowser (same as `posthog_key`)." } } }, "PublicShareMetadata": { "type": "object", "description": "Public share metadata (returned without authentication)", "required": [ "id", "name", "requires_password", "options", "enabled_views" ], "properties": { "enabled_views": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyView" }, "description": "Resolved list of available topology views for this share.\nFiltered by both share configuration and data availability.\nFirst element is the default view." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "name": { "type": "string", "description": "Human-facing name for this share." }, "options": { "$ref": "#/components/schemas/ShareOptions", "description": "What the viewer can see and do." }, "requires_password": { "type": "boolean", "description": "Whether a password must be supplied before the topology is returned." } } }, "RangeEvidence": { "type": "object", "description": "Where an assumed range came from: the far ends whose published addresses it was drawn around,\nand the local devices that saw them. Read from the warning of the scan that created the range.", "required": [ "discovery_id", "seen_by_host_ids", "addresses", "sys_names", "widened_by_vlan" ], "properties": { "addresses": { "type": "array", "items": { "type": "string" }, "description": "The addresses the far ends published for themselves, in the order observed." }, "discovery_id": { "type": "string", "format": "uuid", "description": "The run that created the range, so the reader can open its report." }, "seen_by_host_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The local devices that saw the far ends. A host deleted since is still listed here; it\nresolves to nothing." }, "sys_names": { "type": "array", "items": { "type": "string" }, "description": "The far ends' own `sysName`s, where they sent any." }, "widened_by_vlan": { "type": "boolean", "description": "Whether a shared VLAN widened the range past the conventional prefix." } } }, "RecentDiscovery": { "type": "object", "description": "A recent discovery run with its warnings counted by remedy.", "required": [ "discovery", "warning_summary" ], "properties": { "discovery": { "$ref": "#/components/schemas/Discovery", "description": "The historical run." }, "warning_summary": { "$ref": "#/components/schemas/RunWarningSummary", "description": "The run's warnings counted by remedy. Empty for a run that recorded none." } } }, "ReferralSource": { "type": "string", "description": "How a user first heard about Scanopy, as offered by the onboarding prompt.", "enum": [ "search_engine", "ai_assistant", "youtube", "tiktok", "blog_article", "reddit", "hacker_news", "social_media", "word_of_mouth", "proxmox_community_scripts", "self_hosted", "other", "prefer_not_to_say" ] }, "ReferralSourceRequest": { "type": "object", "description": "Request to submit referral source", "required": [ "referral_source" ], "properties": { "referral_source": { "$ref": "#/components/schemas/ReferralSource", "description": "How the user heard about Scanopy." }, "referral_source_other": { "type": [ "string", "null" ], "description": "Free-text detail, sent when `referral_source` is `other`." } } }, "RegisterRequest": { "type": "object", "description": "Registration request from client", "required": [ "email", "password", "terms_accepted" ], "properties": { "company_url": { "type": [ "string", "null" ], "description": "Honeypot field for bot detection" }, "email": { "type": "string", "format": "email", "description": "Email address for the new account. Must be deliverable." }, "password": { "type": "string", "format": "password", "description": "Password for the new account. Minimum 10 characters.", "writeOnly": true }, "terms_accepted": { "type": "boolean", "description": "Must be `true` — records that the user accepted the terms of service." } } }, "ReleaseNoteResponse": { "type": "object", "description": "One release's notes.", "required": [ "version", "date", "title", "html", "url" ], "properties": { "date": { "type": "string", "format": "date", "description": "The day the release was prepared." }, "html": { "type": "string", "description": "The notes, as HTML rendered from the reviewed markdown, without raw HTML." }, "title": { "type": "string", "description": "The release's headline." }, "url": { "type": "string", "description": "This release on the public changelog." }, "version": { "type": "string", "description": "The release, as semver (e.g. `0.18.0`)." } } }, "RemedyCount": { "type": "object", "description": "The number of a run's warnings on one [`WarningRemedy`] rung.", "required": [ "remedy", "count" ], "properties": { "count": { "type": "integer", "format": "int32", "description": "Warnings on that rung.", "minimum": 0 }, "remedy": { "$ref": "#/components/schemas/WarningRemedy", "description": "The rung these warnings sit on." } } }, "RequestEmailChangeRequest": { "type": "object", "required": [ "new_email" ], "properties": { "current_password": { "type": [ "string", "null" ], "format": "password", "description": "Current password — required if the user already has a password set.\nNot required for OIDC-only users.", "writeOnly": true }, "new_email": { "type": "string", "format": "email", "description": "Address to move the account to. A confirmation link is sent there." } } }, "RescanSettings": { "type": "object", "description": "Scan settings that apply to a single-host rescan.\n\nDeliberately narrower than [`ScanSettings`]: a rescan verifies a known host\nagainst a known port set, so the full-scan mechanism (`is_full_scan`,\n`full_scan_interval`) must not be expressible — promoting a rescan to a\n65,535-port sweep defeats the feature. The remaining omissions are settings\nthat cannot bind on a one-or-two address target.", "properties": { "arp_retries": { "type": [ "integer", "null" ], "format": "int32", "description": "ARP retry rounds. Matters more here than in a sweep: for a rescan, \"did\nit answer\" is the entire answer, so a missed round reads as a dead host.", "minimum": 0 }, "port_scan_batch_size": { "type": [ "integer", "null" ], "description": "Ports scanned concurrently per host.", "minimum": 0 }, "probe_raw_socket_ports": { "type": "boolean", "description": "Whether to probe raw-socket ports 9100-9107. Correctness-affecting: with\nthis off the scanner drops those ports from its results, so a printer's\nknown JetDirect port would look like it had disappeared." }, "scan_rate_pps": { "type": [ "integer", "null" ], "format": "int32", "description": "Port scan probes per second. Operators lower this for fragile devices or\nnoisy IDS, and a rescan must respect that as much as a discovery does.", "minimum": 0 }, "trust_port_only_detections": { "type": "boolean", "description": "Carried into a rescan: a rescan of a host on a routed subnet runs the same evidence check,\nand it would be strange for a host to survive a scan and vanish on rescan, or the reverse." }, "use_npcap_arp": { "type": "boolean", "description": "On Windows, use Npcap broadcast ARP instead of SendARP." } } }, "ResendVerificationRequest": { "type": "object", "description": "Request to resend verification email", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "description": "Address to resend the verification email to." } } }, "ResetPasswordRequest": { "type": "object", "required": [ "token", "password" ], "properties": { "password": { "type": "string", "format": "password", "description": "The new password. Minimum 10 characters.", "writeOnly": true }, "token": { "type": "string", "description": "Single-use token from the password-reset email.", "writeOnly": true } } }, "RunType": { "oneOf": [ { "type": "object", "title": "Scheduled", "required": [ "cron_schedule", "enabled", "type" ], "properties": { "cron_schedule": { "type": "string", "description": "Cron expression deciding when the scan runs." }, "enabled": { "type": "boolean", "description": "Whether the schedule is active." }, "last_run": { "type": [ "string", "null" ], "format": "date-time", "description": "When the scan last ran.", "readOnly": true }, "timezone": { "type": [ "string", "null" ], "description": "IANA timezone for cron evaluation, e.g. \"America/New_York\". None = UTC." }, "type": { "type": "string", "enum": [ "Scheduled" ] } } }, { "type": "object", "title": "Historical", "description": "Historical discovery runs are created by the server and cannot be submitted via API", "required": [ "results", "type" ], "properties": { "results": { "$ref": "#/components/schemas/DiscoveryUpdatePayload", "description": "The recorded outcome of the run." }, "type": { "type": "string", "enum": [ "Historical" ] } } }, { "type": "object", "title": "AdHoc", "required": [ "type" ], "properties": { "last_run": { "type": [ "string", "null" ], "format": "date-time", "description": "When the scan last ran.", "readOnly": true }, "type": { "type": "string", "enum": [ "AdHoc" ] } } } ] }, "RunWarningSummary": { "type": "object", "description": "One run's warnings, counted by remedy.", "required": [ "by_remedy", "actionable", "total" ], "properties": { "actionable": { "type": "integer", "format": "int32", "description": "Warnings whose remedy asks the reader to act ([`WarningRemedy::is_actionable`]).", "minimum": 0 }, "by_remedy": { "type": "array", "items": { "$ref": "#/components/schemas/RemedyCount" }, "description": "Counts per remedy in [`WarningRemedy`] declaration order. Rungs with no warnings are left\nout." }, "total": { "type": "integer", "format": "int32", "description": "Every warning the run recorded.", "minimum": 0 } } }, "SaveOffer": { "type": "string", "description": "Save-offer choices presented during in-app cancellation (Phase 5).", "enum": [ "pause", "discount", "downgrade" ] }, "SaveOfferCoupon": { "type": "object", "description": "Live terms for the configured save-offer coupon, read directly from\nStripe. Used by the cancel modal's Discount panel to render the offer\ndynamically instead of hard-coding the percent/duration.\n\nOnly returned when the coupon would actually catch the user's next\ninvoice — i.e. `next_renewal_at` falls within the coupon's `duration_in_months`\nwindow. Yearly subscribers partway through a cycle whose next renewal\nlands after the coupon's window get `None` from the endpoint and the\ncancel modal's Discount panel doesn't render.\n\n`billing_rate` lets the frontend pick monthly vs yearly copy: a monthly\nsubscriber thinks in terms of \"N months of discount\"; a yearly subscriber\nthinks in terms of \"my next renewal on {date}.\"", "required": [ "percent_off", "duration_in_months", "next_renewal_at", "billing_rate" ], "properties": { "billing_rate": { "$ref": "#/components/schemas/BillingRate", "description": "Billing interval the discount applies to." }, "duration_in_months": { "type": "integer", "format": "int64", "description": "How many months the discount lasts." }, "next_renewal_at": { "type": "string", "format": "date-time", "description": "When the discounted subscription next renews." }, "percent_off": { "type": "integer", "format": "int64", "description": "Discount applied by the retention offer." } } }, "ScanSettings": { "type": "object", "description": "Scan performance settings. Lives on the discovery entity.\nNumeric fields are `Option` — `None` means \"use daemon default\".\nThe daemon unwraps with defaults at point of use.", "properties": { "arp_rate_pps": { "type": [ "integer", "null" ], "format": "int32", "description": "ARP packets per second (default: 50)", "minimum": 0 }, "arp_retries": { "type": [ "integer", "null" ], "format": "int32", "description": "ARP retry rounds for non-responsive targets (default: 2 = 3 total attempts)", "minimum": 0 }, "arp_scan_cutoff": { "type": [ "integer", "null" ], "format": "int32", "description": "ARP scan cutoff prefix. Interfaced subnets larger than this prefix are\ntruncated to this many IPs. Default: 15 (= /15, ~131K IPs).\nLower values scan more IPs — increase arp_rate_pps accordingly.", "minimum": 0 }, "full_scan_interval": { "type": [ "integer", "null" ], "format": "int32", "description": "Run a full 65k port scan every N scans. Other scans use a light port set.\nDefault: 3. Value of 0 means never full scan. Value of 1 means every scan is full.", "minimum": 0 }, "is_full_scan": { "type": "boolean", "description": "Whether this specific scan run should do a full 65k port scan.\nSet by the server before dispatching to the daemon — not user-configurable." }, "max_discovery_duration": { "type": [ "integer", "null" ], "format": "int32", "description": "Hard ceiling on how long a single discovery run may take, in seconds\n(default: 21600 = 6h). When hit, the run force-completes and any hosts\nstill queued are left un-scanned until the next run. Raise this for very\nlarge networks that legitimately need more than the default window.", "minimum": 0 }, "port_scan_batch_size": { "type": [ "integer", "null" ], "description": "Ports scanned concurrently per host (default: 200, clamped 16-1000)", "minimum": 0 }, "probe_raw_socket_ports": { "type": "boolean", "description": "Whether to probe raw-socket ports 9100-9107 (default: false).\nDisabled by default to prevent ghost printing on JetDirect printers." }, "scan_rate_pps": { "type": [ "integer", "null" ], "format": "int32", "description": "Port scan probes per second (default: 500)", "minimum": 0 }, "trust_port_only_detections": { "type": "boolean", "description": "Whether an open TCP port alone is enough to record a host on a subnet the daemon reaches\n*through* something (default: false).\n\nOn a subnet the daemon has an interface on, ARP answers first and this never applies. On a\nrouted one every address arrives enumerated, and a middlebox that completes the TCP handshake\nfor addresses with nothing behind them — a FortiGate session helper, a load balancer, a\nscrubbing appliance — makes every address look open. With this off, such an address is\nrecorded only when something actually answered: a probe that spoke the protocol, an endpoint\nthat replied, a credential that authenticated.\n\nTurning it on restores the older behaviour for hosts whose every open port is one Scanopy\ncannot interrogate. That is a real class of host — a bespoke TCP service on a routed subnet —\nwhich is why the escape hatch exists rather than the rule simply being absolute." }, "use_npcap_arp": { "type": "boolean", "description": "On Windows, use Npcap broadcast ARP instead of SendARP (default: false)" } } }, "ScannedEntityIds": { "type": "object", "description": "Canonical IDs of entities scanned in a discovery session.\n\nPopulated daemon-side at terminal phase from `EntityBuffer`'s `Created`\nentries. Travels with the terminal `DiscoveryUpdatePayload` to the server,\nrides the in-memory `EntityOperation::Created` event published for the\nhistorical Discovery row (the event scope carries `Entity::Discovery` with\nthe full struct, including `run_type::Historical { results }`), then is\nstripped before persisting into the historical Discovery row's JSONB (see\nthe `SqlValue::RunType` bind_value handler in\n`backend/src/server/shared/storage/generic.rs`). Per-entity-service\nsubscribers extract `results.scanned` from the in-memory event and call\n`DiscoveryFkUpdater::update_discovery_fks` to backfill\n`last_discovery_id` / `first_discovery_id` on the matched rows.\n\nNaming: `scanned_*` because the daemon scans entities — some submissions\nmatch existing rows (refresh), others insert new rows. Both populate the\nEntityBuffer with canonical (server-assigned) IDs.", "properties": { "binding_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Service bindings touched by this discovery." }, "found_subnet_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Subnets this discovery found without sweeping: ranges riding in a host request, ranges the\nserver inferred or placed addresses into, and ranges holding an address the scan saw.\n\nFilled by the server, never by a daemon. Kept apart from `subnet_ids` because the digest\nreads that list as the ranges the scan *swept*, and counting a range it only found evidence\nin would report every unobserved host there as stale. The discovery FKs take both." }, "host_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Hosts touched by this discovery." }, "interface_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Interfaces touched by this discovery." }, "ip_address_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "IP addresses touched by this discovery." }, "port_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Ports touched by this discovery." }, "service_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Services touched by this discovery." }, "subnet_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Subnets touched by this discovery." }, "vlan_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "VLANs touched by this discovery." } } }, "ScriptSource": { "oneOf": [ { "type": "object", "title": "HostFile", "description": "An executable already on each scanned host. Nothing but the path is stored in Scanopy.", "required": [ "path", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "HostFile" ] }, "path": { "$ref": "#/components/schemas/HostPath" } } }, { "type": "object", "title": "DaemonFile", "description": "A script file on the daemon's machine; the daemon reads it and runs its text.", "required": [ "path", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "DaemonFile" ] }, "path": { "$ref": "#/components/schemas/DaemonPath" } } }, { "type": "object", "title": "Inline", "description": "The script text, stored on the credential.", "required": [ "value", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "Inline" ] }, "value": { "type": "string", "description": "The script text." } } } ], "description": "Where an SSH credential's script comes from. One type for the stored credential and the wire:\nboth sides hold the same typed paths, so there is nothing to translate." }, "SearchHit": { "oneOf": [ { "type": "object", "title": "Invite", "required": [ "Invite" ], "properties": { "Invite": { "$ref": "#/components/schemas/Invite" } } }, { "type": "object", "title": "Share", "required": [ "Share" ], "properties": { "Share": { "$ref": "#/components/schemas/Share" } } }, { "type": "object", "title": "Site", "required": [ "Site" ], "properties": { "Site": { "$ref": "#/components/schemas/Site" } } }, { "type": "object", "title": "DaemonApiKey", "required": [ "DaemonApiKey" ], "properties": { "DaemonApiKey": { "$ref": "#/components/schemas/DaemonApiKey" } } }, { "type": "object", "title": "UserApiKey", "required": [ "UserApiKey" ], "properties": { "UserApiKey": { "$ref": "#/components/schemas/UserApiKey" } } }, { "type": "object", "title": "User", "required": [ "User" ], "properties": { "User": { "$ref": "#/components/schemas/User" } } }, { "type": "object", "title": "Tag", "required": [ "Tag" ], "properties": { "Tag": { "$ref": "#/components/schemas/Tag" } } }, { "type": "object", "title": "Discovery", "required": [ "Discovery" ], "properties": { "Discovery": { "$ref": "#/components/schemas/Discovery" } } }, { "type": "object", "title": "Daemon", "required": [ "Daemon" ], "properties": { "Daemon": { "$ref": "#/components/schemas/DaemonResponse" } } }, { "type": "object", "title": "Host", "required": [ "Host" ], "properties": { "Host": { "$ref": "#/components/schemas/HostResponse" } } }, { "type": "object", "title": "Service", "required": [ "Service" ], "properties": { "Service": { "$ref": "#/components/schemas/Service" } } }, { "type": "object", "title": "Port", "required": [ "Port" ], "properties": { "Port": { "$ref": "#/components/schemas/Port" } } }, { "type": "object", "title": "Binding", "required": [ "Binding" ], "properties": { "Binding": { "$ref": "#/components/schemas/Binding" } } }, { "type": "object", "title": "IPAddress", "required": [ "IPAddress" ], "properties": { "IPAddress": { "$ref": "#/components/schemas/IPAddress" } } }, { "type": "object", "title": "Interface", "required": [ "Interface" ], "properties": { "Interface": { "$ref": "#/components/schemas/Interface" } } }, { "type": "object", "title": "Credential", "required": [ "Credential" ], "properties": { "Credential": { "$ref": "#/components/schemas/Credential" } } }, { "type": "object", "title": "Subnet", "required": [ "Subnet" ], "properties": { "Subnet": { "$ref": "#/components/schemas/SubnetResponse" } } }, { "type": "object", "title": "Vlan", "required": [ "Vlan" ], "properties": { "Vlan": { "$ref": "#/components/schemas/Vlan" } } }, { "type": "object", "title": "Dependency", "required": [ "Dependency" ], "properties": { "Dependency": { "$ref": "#/components/schemas/Dependency" } } }, { "type": "object", "title": "Topology", "required": [ "Topology" ], "properties": { "Topology": { "$ref": "#/components/schemas/Topology" } } }, { "type": "object", "title": "Snapshot", "required": [ "Snapshot" ], "properties": { "Snapshot": { "$ref": "#/components/schemas/Snapshot" } } } ], "description": "One match, in the shape its entity's list endpoint returns it (a host as a `HostResponse`,\na daemon as a `DaemonResponse`), keyed by entity type." }, "SecretValue": { "oneOf": [ { "type": "object", "title": "Inline", "required": [ "value", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "Inline" ] }, "value": { "type": "string", "description": "The secret itself. Write-only — reads return a redacted placeholder." } } }, { "type": "object", "title": "FilePath", "required": [ "path", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "FilePath" ] }, "path": { "$ref": "#/components/schemas/DaemonPath", "description": "Path to a file on the daemon host holding the secret." } } } ], "description": "Secret value that can be either inline content or a file path on the daemon host." }, "SelectOption": { "type": "object", "description": "A single choice for a `Select` field. `value` is the wire value (serialized enum variant, e.g.\n\"Sha256\"); `label` is the human-facing display text.", "required": [ "value", "label" ], "properties": { "label": { "type": "string", "description": "Human-facing option label." }, "value": { "type": "string", "description": "Value submitted when this option is chosen." } } }, "ServerCapabilities": { "type": "object", "description": "Server capabilities returned on startup/registration", "required": [ "server_version" ], "properties": { "deprecation_warnings": { "type": "array", "items": { "$ref": "#/components/schemas/DeprecationWarning" }, "description": "Deprecation warnings for the daemon" }, "last_unsupported_daemon_version": { "type": [ "string", "null" ], "description": "Daemons at or below this version are rejected by this server. `None` from a server too old\nto send it." }, "minimum_daemon_version": { "type": [ "string", "null" ], "description": "Deprecated: always 0.12.0. Daemons up to 0.18.2 require this field and only log it. Read\n`last_unsupported_daemon_version` instead." }, "server_version": { "type": "string", "description": "Server software version" } } }, "Service": { "allOf": [ { "$ref": "#/components/schemas/ServiceBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "bindings": [ { "created_at": "2026-10-22T23:24:30.798422Z", "first_discovery_id": null, "id": "a2609fdf-a5a1-41dc-9602-83992cfdca81", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-10-22T23:24:30.798422Z", "lineage_id": null, "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "site_id": "550e8400-e29b-41d4-a716-446655440002", "type": "Port", "updated_at": "2026-10-22T23:24:30.798422Z", "valid_from": "2026-10-22T23:24:30.798422Z", "valid_to": null } ], "created_at": "2026-01-15T10:30:00Z", "first_discovery_id": null, "host_id": "550e8400-e29b-41d4-a716-446655440003", "id": "550e8400-e29b-41d4-a716-446655440007", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "nginx", "position": 0, "service_definition": "Consul", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_metadata": null, "virtualization_service_id": null } }, "ServiceBase": { "type": "object", "required": [ "host_id", "site_id", "service_definition", "name", "bindings", "virtualization_service_id", "source", "tags", "position" ], "properties": { "bindings": { "type": "array", "items": { "$ref": "#/components/schemas/Binding" }, "description": "Ports and IP addresses this service is reachable on." }, "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "name": { "type": "string", "description": "Human-facing name for the service." }, "position": { "type": "integer", "format": "int32", "description": "Position of this service in the host's service list (for ordering)" }, "service_definition": { "type": "string", "description": "Which known software this service is, if identified." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "Will be automatically set to Manual for creation through API", "readOnly": true }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/ServiceVirtualization", "description": "Container runtime the service runs in, when it is containerized." }, { "type": "null" } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The container runtime service hosting this container — see the note on\n`HostBase::virtualization_service_id`." } } }, "ServiceCategory": { "type": "string", "enum": [ "NetworkCore", "NetworkAccess", "NetworkAppliance", "RemoteAccess", "Storage", "Backup", "Media", "HomeAutomation", "Hypervisor", "ContainerRuntime", "Container", "Orchestrator", "DNS", "VPN", "Monitoring", "AdBlock", "ReverseProxy", "Workstation", "Mobile", "IoT", "Industrial", "Printer", "Database", "Development", "Dashboard", "MessageQueue", "IdentityAndAccess", "Integration", "Office", "ProjectManagement", "Messaging", "Conferencing", "Telephony", "Email", "Publishing", "Unknown", "Custom", "Scanopy", "OpenPorts", "NetworkIdentities" ] }, "ServiceInput": { "type": "object", "description": "Input for creating or updating a service.\nUsed in both CreateHostRequest and UpdateHostRequest.\nClient must provide a UUID for the service.", "required": [ "id", "service_definition", "name" ], "properties": { "bindings": { "type": "array", "items": { "$ref": "#/components/schemas/BindingInput" }, "description": "Bindings that associate this service with ports/interfaces" }, "id": { "type": "string", "format": "uuid", "description": "Client-provided UUID for this service" }, "name": { "type": "string", "description": "Display name for this service" }, "position": { "type": [ "integer", "null" ], "format": "int32", "description": "Position in the host's service list (for ordering).\nIf omitted on create: appends to end of list.\nIf omitted on update: existing services keep their positions; new services append.\nMust be all specified or all omitted across all services in the request." }, "service_definition": { "type": "string", "description": "Service definition ID (e.g., \"Nginx\", \"PostgreSQL\")" }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags for categorization" }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/ServiceVirtualization", "description": "Container identity (name, id, compose project) if this service is a container." }, { "type": "null" } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The container runtime service hosting this container, if any." } } }, "ServiceOrderField": { "type": "string", "description": "Fields that services can be ordered/grouped by.", "enum": [ "created_at", "name", "updated_at", "host", "site_id", "position", "service_definition", "last_seen_at", "containerized_by", "match_confidence", "source" ] }, "ServiceVirtualization": { "oneOf": [ { "type": "object", "title": "Docker", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/DockerVirtualization" }, "type": { "type": "string", "enum": [ "Docker" ] } } }, { "type": "object", "title": "Podman", "required": [ "details", "type" ], "properties": { "details": { "$ref": "#/components/schemas/PodmanVirtualization" }, "type": { "type": "string", "enum": [ "Podman" ] } } } ], "title": "ServiceVirtualization" }, "SetTagsRequest": { "type": "object", "description": "Request body for setting all tags on an entity", "required": [ "entity_type", "entity_id", "tag_ids" ], "properties": { "entity_id": { "type": "string", "format": "uuid", "description": "The entity ID" }, "entity_type": { "$ref": "#/components/schemas/EntityDiscriminants", "description": "The entity type (e.g., Host, Service, Subnet)" }, "tag_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The new list of tag IDs" } } }, "SetupIntentResponse": { "type": "object", "description": "Response for creating a SetupIntent — the client secret the frontend\nPayment Element uses to collect and confirm a card in-app.", "required": [ "client_secret" ], "properties": { "client_secret": { "type": "string", "description": "Stripe SetupIntent client secret, used to mount the Payment Element." } } }, "SetupRequest": { "type": "object", "description": "Setup request for pre-registration org/site configuration", "required": [ "organization_name" ], "properties": { "organization_name": { "type": "string", "description": "Name for the organization created during setup." }, "site": { "oneOf": [ { "$ref": "#/components/schemas/SiteSetup", "description": "The first site to create alongside the organization. `None` for a\nself-hosted license buyer, whose cloud org gets its site only if it\nlater moves to a cloud plan." }, { "type": "null" } ] } } }, "SetupResponse": { "type": "object", "description": "Response from setup endpoint", "properties": { "site_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Pre-assigned id of the site created at registration, if one was requested." } } }, "Share": { "allOf": [ { "$ref": "#/components/schemas/ShareBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "ShareAccessTokenResponse": { "type": "object", "description": "Access token returned after successful password verification.\n\nThe token is an HS256 JWT tied to the share's `password_hash` — changing\nthe share password implicitly invalidates all outstanding tokens.", "required": [ "access_token", "expires_at" ], "properties": { "access_token": { "type": "string", "description": "Bearer token granting access to this share for the rest of the session." }, "expires_at": { "type": "string", "format": "date-time", "description": "When this record stops being valid." } } }, "ShareBase": { "type": "object", "required": [ "topology_id", "site_id", "created_by", "name", "is_enabled", "expires_at", "allowed_domains", "options", "enabled_views" ], "properties": { "allowed_domains": { "type": [ "array", "null" ], "items": { "type": "string" }, "description": "Domains permitted to embed this share. Empty means no restriction." }, "created_by": { "type": "string", "format": "uuid", "description": "User who created the share." }, "enabled_views": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/TopologyView" }, "description": "Which topology views are enabled for this share.\nNone = all views (subject to data availability). Some(list) = only these views in order.\nFirst element is the default view shown on load." }, "expires_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When this record stops being valid." }, "is_enabled": { "type": "boolean", "description": "Whether the link still resolves. Disabled shares return 404." }, "name": { "type": "string", "description": "Human-facing name for this share." }, "options": { "$ref": "#/components/schemas/ShareOptions", "description": "What the viewer can see and do." }, "password": { "type": [ "string", "null" ], "format": "password", "description": "Plaintext password on ingest; redacted sentinel (`\"********\"`) or `None` on egress.\nNever stored — `password_hash` is the DB column. Wrapped in `SecretString` so\n`Debug`/logging shows `[REDACTED]` during the window between request\ndeserialization and hashing." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "topology_id": { "type": "string", "format": "uuid", "description": "The topology this share exposes." } } }, "ShareOptions": { "type": "object", "description": "Share display options", "required": [ "show_inspect_panel", "show_zoom_controls", "show_export_button", "show_minimap" ], "properties": { "show_export_button": { "type": "boolean", "description": "Viewer sees the export button." }, "show_inspect_panel": { "type": "boolean", "description": "Viewer can open the inspector for a selected element." }, "show_minimap": { "type": "boolean", "description": "Viewer sees the minimap." }, "show_zoom_controls": { "type": "boolean", "description": "Viewer sees the zoom controls." } } }, "ShareTopologyRequest": { "type": "object", "required": [ "view" ], "properties": { "access_token": { "type": [ "string", "null" ], "description": "Server-issued access token obtained from `/verify`. Required when the share has a password." }, "view": { "$ref": "#/components/schemas/TopologyView", "description": "Which topology view to return data for" } } }, "ShareWithTopology": { "type": "object", "description": "Share with topology data (returned after authentication/verification).\n\nReturns the slim topology row (`{ id, site_id, options }`) plus the\n`TopologyData` bundle (entities + the per-view graph built on request). The\nshare viewer composes these with the same `toRenderableTopology` the app\nuses — no server-side merge.", "required": [ "share", "topology", "data", "export_features" ], "properties": { "data": { "$ref": "#/components/schemas/TopologyData", "description": "Entities and graph for the requested view." }, "export_features": { "$ref": "#/components/schemas/ExportFeatures", "description": "Which exports the share creator's plan allows." }, "share": { "$ref": "#/components/schemas/PublicShareMetadata", "description": "Public metadata for the share itself." }, "topology": { "$ref": "#/components/schemas/Topology", "description": "The shared topology record." } } }, "Site": { "allOf": [ { "$ref": "#/components/schemas/SiteBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "effective_stale_after_hours": { "type": "integer", "format": "int64", "description": "`stale_after_hours` with the server's default already applied.\n\nComputed, never stored (excluded from `to_params`). Published so the\nfrontend derives staleness from the *same* number the digest uses rather\nthan re-declaring the default in TypeScript, where the two could drift\nand a host could read stale in the app but current in the digest email.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "example": { "created_at": "2026-01-15T10:30:00Z", "credential_ids": [], "effective_stale_after_hours": 672, "id": "550e8400-e29b-41d4-a716-446655440002", "name": "Home Site", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "stale_after_hours": null, "tags": [], "updated_at": "2026-01-15T10:30:00Z" } }, "SiteBase": { "type": "object", "required": [ "name", "organization_id", "tags", "stale_after_hours" ], "properties": { "credential_ids": { "type": [ "array", "null" ], "items": { "type": "string", "format": "uuid" }, "description": "Credential IDs assigned to this site (hydrated from the junction table, so always present\nin a response). On create, omitting it assigns the organization's site-wide default\ncredentials and a list, even an empty one, is used exactly. On update, omitting it leaves\nthe assignments unchanged." }, "name": { "type": "string", "description": "Human-facing name for this site." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "stale_after_hours": { "type": [ "integer", "null" ], "format": "int64", "description": "How long a discovery-managed entity on this site may go unobserved\nbefore it reads as stale. `None` = unset; callers resolve the effective\nvalue through [`Site::stale_after`], never by reading this directly.\n\nSite-scoped because staleness is only meaningful relative to scan\ncadence, and cadence is a property of a site's discoveries." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." } } }, "SiteOrderField": { "type": "string", "description": "Fields the sites list can be ordered and grouped by.", "enum": [ "created_at", "name", "updated_at", "effective_stale_after_hours" ] }, "SiteSetup": { "type": "object", "description": "Site configuration for setup", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "Name for the site created during setup." } } }, "SiteSummary": { "type": "object", "description": "Per-site summary of entity counts", "required": [ "id", "name", "host_count", "service_count", "subnet_count", "daemon_count" ], "properties": { "daemon_count": { "type": "integer", "format": "int64", "description": "Daemons assigned to this site.", "minimum": 0 }, "host_count": { "type": "integer", "format": "int64", "description": "Hosts currently discovered on this site.", "minimum": 0 }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "name": { "type": "string", "description": "Name of the site." }, "service_count": { "type": "integer", "format": "int64", "description": "Services currently discovered on this site.", "minimum": 0 }, "subnet_count": { "type": "integer", "format": "int64", "description": "Subnets currently known on this site.", "minimum": 0 } } }, "Snapshot": { "allOf": [ { "$ref": "#/components/schemas/SnapshotBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "SnapshotBase": { "type": "object", "required": [ "site_id", "taken_at" ], "properties": { "created_by_user_id": { "type": [ "string", "null" ], "format": "uuid", "description": "User who took the snapshot." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "taken_at": { "type": "string", "format": "date-time", "description": "The point in time this snapshot captures." } } }, "SnmpCheck": { "type": "string", "description": "The net-snmp commands that check a warning against the device, from the daemon's host.\n\nPublished on each warning code's metadata as `snmp_check`, so the UI builds the commands from\nwhat the code declares instead of keeping its own list of codes. Each value is the comparison\nthat tells an operator whether the device returns what the warning says it returned.", "enum": [ "Repeat", "Recount", "Claim", "Unordered", "Table" ] }, "SnmpV3AuthProtocol": { "type": "string", "description": "SNMPv3 USM authentication protocol. MD5 is offered for devices that support nothing else;\nSHA-2 variants beyond SHA-256 are not. Serialized form (e.g. \"Sha256\") is the wire value\nstored in the credential and used as the frontend select option value.", "enum": [ "Md5", "Sha1", "Sha256" ] }, "SnmpV3PrivProtocol": { "type": "string", "description": "SNMPv3 USM privacy (encryption) protocol.", "enum": [ "Aes128", "Aes256" ] }, "SnmpV3SecurityOption": { "type": "string", "description": "One SNMPv3 option added after v3 first shipped (AuthPriv with SHA and AES only).", "enum": [ "NoAuthNoPriv", "AuthNoPriv", "Md5Auth" ] }, "SnmpV3SecurityOptionsGap": { "type": "object", "description": "The SNMPv3 options a credential uses that daemons before 0.18.1 cannot parse.", "required": [ "options" ], "properties": { "options": { "type": "array", "items": { "$ref": "#/components/schemas/SnmpV3SecurityOption" }, "description": "The options the credential uses, each one a reason older daemons reject it." } } }, "SnmpV3Security_SecretValue": { "oneOf": [ { "type": "object", "title": "NoAuthNoPriv", "description": "User name only.", "required": [ "security_level" ], "properties": { "security_level": { "type": "string", "enum": [ "NoAuthNoPriv" ] } } }, { "type": "object", "title": "AuthNoPriv", "description": "Authenticated, not encrypted.", "required": [ "auth_protocol", "auth_password", "security_level" ], "properties": { "auth_password": { "oneOf": [ { "type": "object", "title": "Inline", "required": [ "value", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "Inline" ] }, "value": { "type": "string", "description": "The secret itself. Write-only — reads return a redacted placeholder." } } }, { "type": "object", "title": "FilePath", "required": [ "path", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "FilePath" ] }, "path": { "$ref": "#/components/schemas/DaemonPath", "description": "Path to a file on the daemon host holding the secret." } } } ], "description": "Secret value that can be either inline content or a file path on the daemon host." }, "auth_protocol": { "$ref": "#/components/schemas/SnmpV3AuthProtocol", "description": "Hash algorithm used for authentication." }, "security_level": { "type": "string", "enum": [ "AuthNoPriv" ] } } }, { "type": "object", "title": "AuthPriv", "description": "Authenticated and encrypted.", "required": [ "auth_protocol", "auth_password", "priv_protocol", "priv_password", "security_level" ], "properties": { "auth_password": { "oneOf": [ { "type": "object", "title": "Inline", "required": [ "value", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "Inline" ] }, "value": { "type": "string", "description": "The secret itself. Write-only — reads return a redacted placeholder." } } }, { "type": "object", "title": "FilePath", "required": [ "path", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "FilePath" ] }, "path": { "$ref": "#/components/schemas/DaemonPath", "description": "Path to a file on the daemon host holding the secret." } } } ], "description": "Secret value that can be either inline content or a file path on the daemon host." }, "auth_protocol": { "$ref": "#/components/schemas/SnmpV3AuthProtocol", "description": "Hash algorithm used for authentication." }, "priv_password": { "oneOf": [ { "type": "object", "title": "Inline", "required": [ "value", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "Inline" ] }, "value": { "type": "string", "description": "The secret itself. Write-only — reads return a redacted placeholder." } } }, { "type": "object", "title": "FilePath", "required": [ "path", "mode" ], "properties": { "mode": { "type": "string", "enum": [ "FilePath" ] }, "path": { "$ref": "#/components/schemas/DaemonPath", "description": "Path to a file on the daemon host holding the secret." } } } ], "description": "Secret value that can be either inline content or a file path on the daemon host." }, "priv_protocol": { "$ref": "#/components/schemas/SnmpV3PrivProtocol", "description": "Cipher used for privacy (encryption)." }, "security_level": { "type": "string", "enum": [ "AuthPriv" ] } } } ], "description": "The security material of an SNMPv3 credential, by security level. A level carries exactly\nthe fields it uses, so a credential cannot hold a privacy password it never sends.\n\n`S` is the secret's representation: `SecretValue` where the credential is stored,\n`ResolvableSecret` on the server→daemon wire. Flattened into its parent, so the stored JSON\nkeeps the flat shape it had before levels existed." }, "SnmpWalkCause": { "type": "string", "description": "How a read that returned nothing ended, as the daemon observed it.\n\nEach value is something the daemon saw on the wire, never an inference about the device: a\ntimeout is a request with no response, an error status is the status the device returned.\nCollapsing these into one \"stopped answering\" told an operator whose device had answered every\nrequest with `genErr` that it had gone silent.", "enum": [ "TimedOut", "EmptyResponse", "UnreadableResponse", "TooBig", "GenErr", "NoAccess", "AuthorizationError", "OtherErrorStatus" ] }, "SnmpWalkFailure": { "type": "object", "description": "One group that returned nothing on a device, and how its read ended.", "required": [ "group", "cause" ], "properties": { "cause": { "$ref": "#/components/schemas/SnmpWalkCause" }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, "SnmpWalkGroup": { "type": "string", "description": "An SNMP data group a walk may come up short on.\n\nAn enum rather than a free string so the code derivation below is exhaustive: every group has\nto declare which consequence sentence describes it, and a new one cannot be added without\nchoosing.", "enum": [ "Lldp", "Cdp", "Interfaces", "BridgePortNumbering", "BridgeForwarding", "VlanMembership", "ArpTable", "DeviceInventory", "IpAddresses", "LldpLocalPorts", "VlanNames" ] }, "SshScriptOutcome": { "type": "string", "description": "How one script run ended.", "enum": [ "Applied", "NonZeroExit", "TimedOut", "InvalidOutput", "OutputTooLarge", "HostKeyMismatch", "AuthenticationFailed", "ConnectionFailed", "Unknown" ] }, "SshScriptRun": { "type": "object", "description": "What one SSH script did on one host, carried on the run's results.", "required": [ "ip", "outcome", "duration_ms" ], "properties": { "applied_keys": { "type": "array", "items": { "type": "string" }, "description": "Keys whose values were applied, as paths (`model`, `interfaces[].mac`)." }, "detail": { "type": [ "string", "null" ], "description": "The end of stderr, or the parse error, capped at [`MAX_STDERR_EXCERPT_BYTES`]." }, "duration_ms": { "type": "integer", "format": "int64", "description": "How long the script ran, in milliseconds.", "minimum": 0 }, "exit_code": { "type": [ "integer", "null" ], "format": "int32", "description": "The script's exit status, when it exited.", "minimum": 0 }, "ip": { "type": "string", "description": "The address the script ran at." }, "outcome": { "$ref": "#/components/schemas/SshScriptOutcome" }, "rejected_keys": { "type": "array", "items": { "type": "string" }, "description": "Keys that were present and not applied, as paths (`interfaces[0].vlan`): keys the\ncontract does not know, and values that did not parse." } } }, "Subnet": { "allOf": [ { "$ref": "#/components/schemas/SubnetBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "cidr": "192.168.1.0/24", "cidr_source": "DaemonSelfReport", "created_at": "2026-01-15T10:30:00Z", "description": "Local area network", "first_discovery_id": null, "id": "550e8400-e29b-41d4-a716-446655440004", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "name": "LAN", "name_source": "Unspecified", "site_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "subnet_type": "Lan", "subnet_type_source": "Unspecified", "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_service_id": null, "vlan_ids": [] } }, "SubnetBase": { "allOf": [ { "$ref": "#/components/schemas/SubnetCidr", "description": "The subnet's range and how far it can be trusted.\n\nOne field rather than two, which is what keeps the pair from drifting: a range and the\nconfidence in it are one fact, and assigning `cidr` alone is how a guess ends up\nindistinguishable from a reading. It shipped as two, and three separate places had already\ngrown their own rules for writing the source half behind the applier's back.\n\nSerialises as the two flat keys `cidr` and `cidr_source`, so the wire shape is unchanged." }, { "$ref": "#/components/schemas/SubnetName", "description": "Human-facing name for this subnet, with the source that named it.\n\nTwo flat wire keys, `name` and `name_source`, so a client that sends only `name` still\ndeserialises, at `Unspecified`." }, { "$ref": "#/components/schemas/SubnetTypeAttributed", "description": "What type of subnet this is — physical, virtual, container bridge, and so on — with the\nsource that typed it. Two flat wire keys, `subnet_type` and `subnet_type_source`." }, { "type": "object", "required": [ "site_id", "virtualization_service_id", "source", "tags" ], "properties": { "description": { "type": [ "string", "null" ], "description": "Free-text notes about the subnet." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "Will be automatically set to Manual for creation through API" }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The container runtime service that owns this bridge network.\n\nLoad-bearing for dedup: the same CIDR on two different Docker daemons is two distinct\nsubnets, so bridge rows only merge when this matches as well as the CIDR and site.\nA foreign key rather than a field inside a JSONB blob because a stale value here is\nprecisely what made a scan add a duplicate bridge row every time (GH #650) — now it\ncannot be written at all." }, "vlan_ids": { "type": "array", "items": { "type": "string", "format": "uuid", "readOnly": true }, "description": "VLANs associated with this subnet, through the `subnet_vlans` junction (the reverse of\n`Vlan::subnet_ids`). Hydrated by `SubnetService` on read; it is not a column on `subnets`,\nso anything sent here on create/update is ignored by `to_params`." } } } ] }, "SubnetCidr": { "type": "object", "required": [ "cidr" ], "properties": { "cidr": { "type": "string", "description": "Subnet in CIDR notation, IPv4 or IPv6.", "examples": [ "192.168.1.0/24" ], "pattern": "^[0-9A-Fa-f.:]+/\\d{1,3}$" }, "cidr_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "SubnetName": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "Human-facing name for this subnet." }, "name_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "SubnetOrderField": { "type": "string", "description": "Fields that subnets can be ordered/grouped by.", "enum": [ "created_at", "name", "cidr", "subnet_type", "updated_at", "site_id", "last_seen_at", "source", "managed_by", "utilization" ] }, "SubnetResponse": { "allOf": [ { "$ref": "#/components/schemas/Subnet" }, { "type": "object", "required": [ "used_addresses", "usable_addresses", "nesting_depth", "nesting_root_id", "range_evidence" ], "properties": { "nesting_depth": { "type": "integer", "format": "int32", "description": "How many ranges this one sits inside: 0 for a range nested in nothing.", "readOnly": true, "minimum": 0 }, "nesting_root_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The outermost range of the tree this one sits in: itself when it holds others and sits in\nnone. `null` for a range that nests with nothing.", "readOnly": true }, "parent_subnet_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most specific other subnet on the same site whose range contains this one. `null` when\nnone does. Children are the subnets whose parent is this one.", "readOnly": true }, "range_evidence": { "oneOf": [ { "$ref": "#/components/schemas/RangeEvidence", "description": "For a range Scanopy assumed from what LLDP/CDP neighbours published, the far ends it was\ndrawn around and the devices that saw them. `null` for every other range, and for an\nassumed one no scan created." }, { "type": "null" } ], "readOnly": true }, "usable_addresses": { "type": "integer", "format": "int64", "description": "Addresses a host can hold in this range. Saturates for IPv6 ranges wider than a `/64`.", "readOnly": true, "minimum": 0 }, "used_addresses": { "type": "integer", "format": "int64", "description": "Distinct addresses held in this range, including those filed in subnets nested inside it.", "readOnly": true, "minimum": 0 } } } ], "description": "A subnet together with what is derived from the rest of its site." }, "SubnetType": { "type": "string", "enum": [ "Internet", "Remote", "Gateway", "VpnTunnel", "Dmz", "Lan", "WiFi", "IoT", "Guest", "DockerBridge", "PodmanBridge", "MacVlan", "IpVlan", "Management", "Storage", "Loopback", "Unknown" ] }, "SubnetTypeAttributed": { "type": "object", "required": [ "subnet_type" ], "properties": { "subnet_type": { "$ref": "#/components/schemas/SubnetType" }, "subnet_type_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "TableColumn": { "type": "object", "description": "A list-table column that is not a registry field: a value the table computes, reads from\nanother endpoint, or keeps out of CSV export (`created_at`, `last_seen_at`). Declared here so\nthe column menu files it under a group the backend chose. CSV, diagram Shape Data and the\ninspector read only fields, never these.", "required": [ "id", "group" ], "properties": { "group": { "$ref": "#/components/schemas/FieldGroup" }, "id": { "type": "string", "description": "The column's key in the list table, and so in saved column preferences." } } }, "TableDensity": { "type": "string", "enum": [ "comfortable", "compact" ] }, "Tag": { "allOf": [ { "$ref": "#/components/schemas/TagBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ], "example": { "color": "Green", "created_at": "2026-01-15T10:30:00Z", "description": "Production environment resources", "icon": null, "id": "550e8400-e29b-41d4-a716-44665544000a", "lineage_id": null, "name": "production", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "position": 1, "tag_group_id": "550e8400-e29b-41d4-a716-446655440010", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "TagBase": { "type": "object", "required": [ "name", "color", "organization_id", "tag_group_id", "position" ], "properties": { "color": { "$ref": "#/components/schemas/Color", "description": "Colour the tag is drawn in." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the tag." }, "icon": { "oneOf": [ { "$ref": "#/components/schemas/TagIcon", "description": "Icon drawn on the tag. Application tags always use the application icon." }, { "type": "null" } ] }, "name": { "type": "string", "description": "Human-facing name for this tag." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "position": { "type": [ "integer", "null" ], "format": "int32", "description": "The tag's place in its group's order, from 1. `null` when the tag is in no group. Set by\nthe server: a tag joins the end of its group, and the group's tag order endpoint reorders.", "readOnly": true }, "tag_group_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The tag group this tag belongs to, if any. An entity holds at most one tag of a group." } } }, "TagGroup": { "allOf": [ { "$ref": "#/components/schemas/TagGroupBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ], "description": "A named set of tags of which an entity holds at most one, in an order the organization sets,\noptionally limited to some entity types.", "example": { "created_at": "2026-01-15T10:30:00Z", "description": "Which stage of delivery an asset serves.", "entity_types": null, "group_type": "Custom", "id": "550e8400-e29b-41d4-a716-446655440010", "name": "Environment", "ordered": true, "organization_id": "550e8400-e29b-41d4-a716-446655440001", "scope_computed": false, "updated_at": "2026-01-15T10:30:00Z" } }, "TagGroupBase": { "type": "object", "required": [ "name", "description", "organization_id", "group_type", "entity_types", "ordered", "scope_computed" ], "properties": { "description": { "type": [ "string", "null" ], "description": "What the group is for." }, "entity_types": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/EntityDiscriminants" }, "description": "The entity types this group's tags can be applied to. `null` allows every taggable type the\ngroup type admits: Status leaves out types that carry a status of their own. Fixed for\nVisibility, which applies to hosts." }, "group_type": { "$ref": "#/components/schemas/TagGroupType", "description": "Whether this is a system group or one the organization named. Set by the server: groups\ncreated through the API are always `Custom`.", "readOnly": true }, "name": { "type": "string", "description": "The group's name. A system group's name is fixed." }, "ordered": { "type": "boolean", "description": "Whether the group's tags have an order the organization sets. An unordered group lists its\ntags by name. Fixed for system groups: statuses are ordered, applications are not." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "scope_computed": { "type": "boolean", "description": "Whether `entity_types` is computed from entity metadata (Status, Visibility) rather than set\nby the organization. A computed scope is read-only and never stored.", "readOnly": true } } }, "TagGroupOrderField": { "type": "string", "description": "Fields the tag groups list can be ordered and grouped by.", "enum": [ "created_at", "name", "updated_at", "group_type", "ordered" ] }, "TagGroupType": { "type": "string", "description": "What kind of tag group this is.\n\n`Application`, `Status` and `Visibility` are system groups: every organization has exactly one\nof each, and they can't be renamed, deleted or recreated. Application tags drive the\napplication view. Status tags track where an asset is on its way from planned to\ndecommissioned, on entities that have no status of their own. A host holding a Visibility tag\nis left out of topology. `Custom` groups are named by the organization.", "enum": [ "Application", "Status", "Visibility", "Custom" ] }, "TagIcon": { "type": "string", "description": "A lucide icon a person chose for a tag.\n\nA newtype because `lucide_icons::Icon` derives neither `Eq` nor `Hash`, which every entity\nbase needs. The icon is a fieldless enum, so its discriminant is a complete identity." }, "TagOrderField": { "type": "string", "description": "Fields that tags can be ordered/grouped by.", "enum": [ "created_at", "name", "color", "updated_at", "tag_group", "icon" ] }, "TagOrderRequest": { "type": "object", "description": "Request body for setting a tag group's tag order", "required": [ "tag_ids" ], "properties": { "tag_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Every tag in the group, each once, in the order to list them." } } }, "Target": { "type": "string", "enum": [ "DaemonHost", "Site", "Hosts" ] }, "TestReachabilityRequest": { "type": "object", "description": "Request to test reachability of a daemon URL.", "required": [ "url" ], "properties": { "check_health": { "type": "boolean", "description": "If true, also perform an HTTP GET to {url}/health after the TCP check" }, "url": { "type": "string", "description": "Full URL of the daemon (e.g. \"https://daemon.example.com:60073\")" } } }, "TestReachabilityResponse": { "type": "object", "description": "Response from a reachability test.", "required": [ "reachable" ], "properties": { "error": { "type": [ "string", "null" ], "description": "Error message if not reachable" }, "health": { "type": [ "boolean", "null" ], "description": "Health check result (only present when check_health was true)" }, "reachable": { "type": "boolean", "description": "Whether the TCP connection succeeded" } } }, "TimestampStyle": { "type": "string", "enum": [ "relative", "absolute" ] }, "Topology": { "allOf": [ { "$ref": "#/components/schemas/TopologyBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "TopologyBase": { "type": "object", "required": [ "site_id", "options" ], "properties": { "options": { "$ref": "#/components/schemas/TopologyOptions", "description": "Saved layout and view settings for this topology." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." } } }, "TopologyData": { "type": "object", "description": "Bundle of entities + the built graph that feed the topology render, export,\nand share pipelines.\n\nLoaded by [`crate::server::topology::service::main::TopologyService::get_topology_data`]\nfor either the live view (`snapshot_id = None`) or a point-in-time snapshot\n(`snapshot_id = Some(id)`). The per-view `nodes`/`edges` are built on request\nfrom these entities + the site's grouping options\n(`build_view_graphs`) — they are not persisted. The frontend selects the\nactive view's slice client-side.", "required": [ "hosts", "ip_addresses", "subnets", "dependencies", "ports", "bindings", "interfaces", "services", "vlans", "tags", "application_group_id" ], "properties": { "application_group_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The organization's Application tag group. Its tags place entities in applications.\nShipped here because share and embed viewers can't list tag groups." }, "available_views": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyView" }, "description": "Views whose data is present in this entity set (L3/Workloads always;\nL2 Physical iff LLDP/CDP neighbors exist; Application iff app-flagged\ntags are used). The topology tab restricts a snapshot's view picker to\nthese — you can't set up SNMP or create app tags on a historical\nsnapshot — while the live view shows all views with setup prompts." }, "bindings": { "type": "array", "items": { "$ref": "#/components/schemas/Binding" }, "description": "Service bindings included in this topology." }, "dependencies": { "type": "array", "items": { "$ref": "#/components/schemas/Dependency" }, "description": "Dependencies included in this topology." }, "edges": { "type": "object", "description": "Connections between the nodes of the built graph.", "additionalProperties": { "type": "array", "items": { "$ref": "#/components/schemas/Edge" } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } }, "filtered_out": { "type": "object", "description": "How many entities of each type a server-side metadata filter removed, by the filter that\nremoved them.\n\nServer-filtered entities never reach the browser, so this is the only way the frontend can\nsay \"171 interfaces hidden by By link\" rather than presenting an empty view as an empty\nsite. Keyed by entity and filter only, with no view: the hide-set is per view but a drop\nis not — an entity is removed from the one shared bundle only when *every* view that could\nrender it hides it (see `metadata_filter`).\n\nEmpty when nothing was filtered, which is the common case.", "additionalProperties": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 0 }, "propertyNames": { "type": "string", "description": "The type of metadata filter. One variant per conceptually-distinct filter\nacross the app — Category (on Service), Virtualization (on Host), and so\non. Kept narrow on purpose: adding a new filter means adding a variant\nhere + a `HasFilterValues` impl on the relevant entity.", "enum": [ "Category", "Virtualization", "LinkState", "Staleness", "OperStatus", "GuestState" ] } }, "propertyNames": { "type": "string", "enum": [ "Site", "Vlan", "Subnet", "Host", "Service", "IPAddress", "Interface", "Port", "Binding", "Discovery", "Daemon", "DaemonApiKey", "Tag", "TagGroup", "User", "UserApiKey", "Credential", "Organization", "Invite", "Topology", "Snapshot", "Share", "Dependency" ] } }, "hosts": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyHost" }, "description": "Hosts included in this topology, each carrying the title the map and the host list share." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyInterface" }, "description": "Interfaces included in this topology, each carrying its metadata filter values." }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "IP addresses included in this topology." }, "neighbours": { "type": "array", "items": { "$ref": "#/components/schemas/InterfaceNeighborRow" }, "description": "Resolved LLDP/CDP neighbour adjacencies (GH #701): the merged read-model of\n`interface_neighbor_interfaces` + `interface_neighbor_hosts`, replacing the old\n`Interface.neighbor` scalar now that a port can anchor several links." }, "nodes": { "type": "object", "description": "Per-view graph built on request from the entities above + grouping\noptions. Keyed by view so switching the active perspective is a\nclient-side slice selection.", "additionalProperties": { "type": "array", "items": { "$ref": "#/components/schemas/Node" } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/Port" }, "description": "Ports included in this topology." }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/TopologyService" }, "description": "Services included in this topology, each carrying its metadata filter values." }, "subnets": { "type": "array", "items": { "$ref": "#/components/schemas/Subnet" }, "description": "Subnets included in this topology." }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/Tag" }, "description": "Tags assigned to this entity." }, "vlans": { "type": "array", "items": { "$ref": "#/components/schemas/Vlan" }, "description": "VLANs included in this topology." } } }, "TopologyHost": { "allOf": [ { "$ref": "#/components/schemas/Host" }, { "type": "object", "properties": { "device_type": { "$ref": "#/components/schemas/DeviceType", "description": "What this host is, as a device: the device type its icon shows. See [`Host::device_type`].", "readOnly": true }, "display_name": { "type": [ "string", "null" ], "description": "What to call this host when `name` is empty: its hostname, sysName, chassis id or first\naddress, whichever it has. `None` when nothing identifies it.\n\nThe same ladder, and the same value, as `HostResponse.display_name` and the host\ncontainer's `header` — a host cannot be called one thing on the map and another in the\nlist.", "readOnly": true }, "filter_values": { "type": "object", "description": "This host's value for each metadata filter it takes part in, keyed by filter. See\n[`TopologyData::stamp_filter_values`].", "additionalProperties": { "type": "string", "readOnly": true }, "propertyNames": { "allOf": [ { "type": "string", "description": "The type of metadata filter. One variant per conceptually-distinct filter\nacross the app — Category (on Service), Virtualization (on Host), and so\non. Kept narrow on purpose: adding a new filter means adding a variant\nhere + a `HasFilterValues` impl on the relevant entity.", "enum": [ "Category", "Virtualization", "LinkState", "Staleness", "OperStatus", "GuestState" ] }, { "type": "object", "readOnly": true } ] } } } } ], "description": "A [`Host`] as the topology bundle ships it: the domain row plus the title every surface has to\nagree on.\n\nThe ladder itself stays on [`Host::display_name`] — this only carries its result. The bundle\nhas to carry it per host because the frontend looks a host up *by id* at sites where no node\nfor that host is guaranteed to be in the current view (a service card naming its parent, a\n`hostA ↔ hostB` edge label, a dependency target), so [`Node::header`] cannot be the only place\nthe name exists.\n\nA wrapper rather than a field on [`Host`] itself: `Host` is the row every other endpoint and\nthe daemon protocol serialize, where a computed title would always be absent and read as \"this\nhost has no name\"." }, "TopologyInterface": { "allOf": [ { "$ref": "#/components/schemas/Interface" }, { "type": "object", "properties": { "filter_values": { "type": "object", "description": "This interface's value for each metadata filter it takes part in, keyed by filter. See\n[`TopologyData::stamp_filter_values`].", "additionalProperties": { "type": "string", "readOnly": true }, "propertyNames": { "allOf": [ { "type": "string", "description": "The type of metadata filter. One variant per conceptually-distinct filter\nacross the app — Category (on Service), Virtualization (on Host), and so\non. Kept narrow on purpose: adding a new filter means adding a variant\nhere + a `HasFilterValues` impl on the relevant entity.", "enum": [ "Category", "Virtualization", "LinkState", "Staleness", "OperStatus", "GuestState" ] }, { "type": "object", "readOnly": true } ] } } } } ], "description": "An [`Interface`] as the topology bundle ships it: the domain row plus its metadata filter\nvalues.\n\nA wrapper rather than a field on [`Interface`] itself, for the same reason as\n[`TopologyService`]: the shared row is serialized by every interface endpoint, where a\ntopology-only value would always be absent." }, "TopologyLocalOptions": { "type": "object", "description": "Viewer settings. `hide_edge_types` and `tag_filter` are per view; the rest apply to every view.", "properties": { "bundle_edges": { "type": "boolean", "description": "Collapse parallel edges between the same pair of nodes into one.", "default": true }, "hide_edge_types": { "type": "object", "description": "Edge types to leave out of the drawing, per view. A view with no entry hides the edge\ntypes that view hides by default. Unknown edge types and views are dropped on read rather\nthan failing the topology.", "default": { "Application": [ "ContainerRuntime" ], "L2Physical": [ "SameHost" ], "L3Logical": [ "Hypervisor", "SameContainer" ], "Workloads": [ "RequestPath", "HubAndSpoke", "PhysicalLink", "NeighborLink" ] }, "additionalProperties": { "type": "array", "items": { "$ref": "#/components/schemas/EdgeTypeDiscriminants" } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } }, "no_fade_edges": { "type": "boolean", "description": "Keep unrelated edges at full opacity when something is selected.", "default": false }, "show_minimap": { "type": "boolean", "description": "Show the minimap.", "default": true }, "tag_filter": { "type": "object", "description": "Tags whose entities are left out, per view. A view with no entry hides nothing by tag.", "default": {}, "additionalProperties": { "$ref": "#/components/schemas/TopologyTagFilter" }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } } } }, "TopologyOptions": { "type": "object", "required": [ "local", "request" ], "properties": { "local": { "$ref": "#/components/schemas/TopologyLocalOptions", "description": "Settings applied in the viewer, which do not change what the server returns." }, "request": { "$ref": "#/components/schemas/TopologyRequestOptions", "description": "Settings that change how the server builds the graph." } } }, "TopologyRequestOptions": { "type": "object", "properties": { "container_rules": { "type": "object", "description": "Rules deciding how nodes are grouped into containers.", "default": { "Application": [ { "id": "b61ff00f-03fe-478f-922f-26c5c9e76dd7", "rule": { "ByApplication": { "tag_ids": [] } } } ], "L2Physical": [ { "id": "a22641af-b046-4940-9b4c-875c68f8eafe", "rule": "ByHost" } ], "L3Logical": [ { "id": "e05ac25c-09ec-4858-876a-2650eb6e5d62", "rule": "BySubnet" }, { "id": "7e39eeb0-7d32-4562-b84a-04d418861f4f", "rule": "MergeContainerBridges" } ], "Workloads": [ { "id": "a22641af-b046-4940-9b4c-875c68f8eafe", "rule": "ByHost" } ] }, "additionalProperties": { "type": "array", "items": { "$ref": "#/components/schemas/IdentifiedRule_ContainerRule" } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } }, "element_rules": { "type": "array", "items": { "$ref": "#/components/schemas/IdentifiedRule_ElementRule" }, "description": "Rules deciding how entities are placed and inlined within containers. A missing field\ntakes the rules for an organization with no stated use case.", "default": [ { "id": "a99d6b46-68d6-43c4-a525-490a3b91fe37", "rule": "ByTrunkPort" }, { "id": "8fe4cd9e-fab0-440a-b1e7-fd5ab7d59e88", "rule": "ByVLAN" }, { "id": "f967cec4-5269-46c1-9491-cfd8564fa019", "rule": "ByPortOpStatus" }, { "id": "50a1ee48-9fcf-42b0-a125-62d00e6b9280", "rule": { "ByServiceCategory": { "categories": [ "NetworkCore", "NetworkAccess", "NetworkAppliance", "RemoteAccess", "Workstation", "Mobile", "IoT", "Printer", "OpenPorts", "NetworkIdentities" ], "is_infra_rule": true, "title": "Infrastructure" } } }, { "id": "81c93f96-89ea-4939-bc26-3a2dfb6570ff", "rule": { "ByTag": { "tag_ids": [], "title": null } } }, { "id": "f61d5397-b585-4834-b712-1d6c80fb5232", "rule": "ByHypervisor" }, { "id": "d60f75db-1e3d-438f-af5b-dad909acab79", "rule": "ByContainerRuntime" }, { "id": "34dbdc76-1ca6-40db-a080-fa6f763b746d", "rule": "ByStack" } ] }, "element_sort": { "type": "object", "description": "How nodes are ordered inside containers, per view. A view with no entry uses the\nlayout's own order.", "default": {}, "additionalProperties": { "$ref": "#/components/schemas/ElementSort" }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } }, "hide_entities": { "type": "object", "description": "Entity types hidden per view. Keyed by TopologyView, values are entity\ntypes (matching those declared as container/element/inline in the\nview's element_config). Hides every manifestation of the entity in\nthat view — element nodes, container nodes, and inline rows on\nelement cards. Supersedes the old `hide_ports` (L3-only, inline-only).", "default": {}, "additionalProperties": { "type": "array", "items": { "$ref": "#/components/schemas/EntityDiscriminants" } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } }, "hide_metadata_values": { "type": "object", "description": "Generic per-(view, entity, filter) hide-set for metadata filters\n(Category, Virtualization, etc). Supersedes the old\n`hide_service_categories`; nested so JSON keys are strings all the\nway down.", "default": { "Application": { "Service": { "Category": [ "OpenPorts" ] } }, "L2Physical": { "Interface": { "LinkState": [ "Unlinked" ] }, "Service": { "Category": [ "OpenPorts" ] } }, "L3Logical": { "Service": { "Category": [ "OpenPorts" ] } }, "Workloads": { "Service": { "Category": [ "OpenPorts" ] } } }, "additionalProperties": { "type": "object", "additionalProperties": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } }, "propertyNames": { "type": "string", "description": "The type of metadata filter. One variant per conceptually-distinct filter\nacross the app — Category (on Service), Virtualization (on Host), and so\non. Kept narrow on purpose: adding a new filter means adding a variant\nhere + a `HasFilterValues` impl on the relevant entity.", "enum": [ "Category", "Virtualization", "LinkState", "Staleness", "OperStatus", "GuestState" ] } }, "propertyNames": { "type": "string", "enum": [ "Site", "Vlan", "Subnet", "Host", "Service", "IPAddress", "Interface", "Port", "Binding", "Discovery", "Daemon", "DaemonApiKey", "Tag", "TagGroup", "User", "UserApiKey", "Credential", "Organization", "Invite", "Topology", "Snapshot", "Share", "Dependency" ] } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } } } }, "TopologyService": { "allOf": [ { "$ref": "#/components/schemas/Service" }, { "type": "object", "properties": { "filter_values": { "type": "object", "description": "This service's value for each metadata filter it takes part in, keyed by filter. See\n[`TopologyData::stamp_filter_values`].", "additionalProperties": { "type": "string", "readOnly": true }, "propertyNames": { "allOf": [ { "type": "string", "description": "The type of metadata filter. One variant per conceptually-distinct filter\nacross the app — Category (on Service), Virtualization (on Host), and so\non. Kept narrow on purpose: adding a new filter means adding a variant\nhere + a `HasFilterValues` impl on the relevant entity.", "enum": [ "Category", "Virtualization", "LinkState", "Staleness", "OperStatus", "GuestState" ] }, { "type": "object", "readOnly": true } ] } } } } ], "description": "A [`Service`] as the topology bundle ships it: the domain row plus its metadata filter values.\n\nA wrapper rather than a field on [`Service`] itself: `Service` is the row every other endpoint\nand the daemon protocol serialize, where a topology-only value would always be absent." }, "TopologyTagFilter": { "type": "object", "description": "Filter settings for hiding entities by tag in topology visualization.", "properties": { "hidden_tag_ids": { "type": "object", "description": "Tag IDs to hide, per entity type. An entity of that type carrying one of them is hidden.", "additionalProperties": { "type": "array", "items": { "type": "string", "format": "uuid" } }, "propertyNames": { "type": "string", "enum": [ "Site", "Vlan", "Subnet", "Host", "Service", "IPAddress", "Interface", "Port", "Binding", "Discovery", "Daemon", "DaemonApiKey", "Tag", "TagGroup", "User", "UserApiKey", "Credential", "Organization", "Invite", "Topology", "Snapshot", "Share", "Dependency" ] } } } }, "TopologyView": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] }, "TransportProtocol": { "type": "string", "enum": [ "Udp", "Tcp" ] }, "TupleUnit": { "type": "object", "description": "No payload. Present only so the envelope keeps its shape." }, "UncoveredIntegration": { "type": "object", "description": "An integration the latest scans found on hosts with no credential of it, still uncovered now.", "required": [ "integration", "host_ids", "site_ids" ], "properties": { "host_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The hosts running its service, which the create flow pre-assigns when the integration's\ncredentials can only be pinned to hosts." }, "integration": { "$ref": "#/components/schemas/CredentialIntegration", "description": "The integration a credential would add." }, "site_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The sites those hosts are on, which the create flow pre-assigns instead when its\ncredentials can apply to a whole site." } } }, "UnmatchedNeighbour": { "type": "object", "description": "A neighbour advertised by a local interface whose far end could not be placed on a host.", "required": [ "host_id", "if_descr", "identifier", "sys_name", "address" ], "properties": { "address": { "type": [ "string", "null" ], "description": "The address the far end published for itself — `lldpRemManAddr`, a `NetworkAddress` port\nid, or `cdpCacheAddress` — where it sent a usable one.\n\nCarried because it is the difference between two reports that otherwise read identically:\n\"the far end told us where it lives and this site holds no such address\" is a device\nnobody has scanned, while \"it told us nothing\" is a device that cannot be placed no matter\nhow much of the network is scanned. Without it, deciding which of the two a fleet is\nlooking at costs another round trip to the operator (GH #668)." }, "host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The local device that saw the neighbour, not the far end — the far end is what could not\nbe identified. `None` once that host has been deleted." }, "identifier": { "type": "string", "description": "The chassis ID (LLDP) or device id (CDP) that did not identify one host." }, "if_descr": { "type": "string", "description": "The local interface that advertised the neighbour." }, "sys_name": { "type": [ "string", "null" ], "description": "The far end's advertised `sysName`, where it sent one." } } }, "UnresolvedPort": { "type": "object", "description": "A neighbour whose far-end host resolved but whose far-end *port* did not.", "required": [ "host_id", "if_descr", "remote_host_id", "port_id", "port_desc" ], "properties": { "host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The local device that saw the neighbour, and the port it saw it on. `None` once that host\nhas been deleted." }, "if_descr": { "type": "string", "description": "The local interface that advertised the neighbour." }, "port_desc": { "type": [ "string", "null" ], "description": "`lldpRemPortDesc`, the last-resort tier. Present because \"the id failed and the description\nwas empty\" and \"both were tried and neither matched\" call for different fixes." }, "port_id": { "type": [ "string", "null" ], "description": "The advertised port id in `Debug` form, which carries subtype and value together\n(`MacAddress(\"00:ad:24:af:4e:00\")`, `InterfaceName(\"2\")`). Both halves are needed: the\nsubtype says which tier ran and the value says what it looked for." }, "remote_host_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The far-end device, already resolved — this is what makes it distinct from\n[`UnmatchedNeighbour`]. `None` once that host has been deleted." } } }, "UnseenReleaseNotes": { "type": "object", "description": "The releases the caller hasn't dismissed.", "required": [ "releases", "auto_open" ], "properties": { "auto_open": { "type": "boolean", "description": "Whether the modal opens on its own: true when any unseen release asks\nfor it, including ones left out for the changelog." }, "changelog_url": { "type": [ "string", "null" ], "description": "The full changelog, present when older unseen releases were left out." }, "releases": { "type": "array", "items": { "$ref": "#/components/schemas/ReleaseNoteResponse" }, "description": "Newest first. Empty when there is nothing new." } } }, "UpdateHostRequest": { "type": "object", "description": "Request type for updating a host with its children.\nUses the same input types as CreateHostRequest.\nServer will sync children (create new, update existing, delete removed) only if provided.", "required": [ "id", "name", "tags" ], "properties": { "credential_assignments": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/CredentialAssignment" }, "description": "Credential assignments for this host.\nIf provided, replaces all existing credential assignments." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the host." }, "expected_updated_at": { "type": [ "string", "null" ], "format": "date-time", "description": "Optional: expected updated_at timestamp for optimistic locking." }, "hostname": { "type": [ "string", "null" ], "description": "Hostname as resolved or reported by the host." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "interfaces": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/InterfaceInput" }, "description": "Interfaces to sync with this host.\nIf Some, server will create/update/delete to match this list.\nIf None, existing interfaces are preserved." }, "ip_addresses": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/IPAddressInput" }, "description": "Interfaces to sync with this host.\nIf Some, server will create/update/delete to match this list.\nIf None, existing ip_addresses are preserved." }, "name": { "type": "string", "description": "Human-facing name for the host." }, "ports": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/PortInput" }, "description": "Ports to sync with this host.\nIf Some, server will create/update/delete to match this list.\nIf None, existing ports are preserved." }, "services": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/ServiceInput" }, "description": "Services to sync with this host.\nIf Some, server will create/update/delete to match this list.\nIf None, existing services are preserved." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "virtualization_interface_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The interface on the virtualizing host that presents this host, for a network identity." }, "virtualization_metadata": { "oneOf": [ { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." }, { "type": "null" } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } } }, "UpdatePasswordRequest": { "type": "object", "required": [ "new_password" ], "properties": { "current_password": { "type": [ "string", "null" ], "format": "password", "description": "Current password — required if the user already has a password set.\nNot required for OIDC-only users adding their first password.", "writeOnly": true }, "new_password": { "type": "string", "format": "password", "description": "New password to set", "writeOnly": true } } }, "UpdatePoNumberRequest": { "type": "object", "description": "Replace the purchase order number printed on future invoices.", "properties": { "po_number": { "type": [ "string", "null" ], "description": "New PO number; empty or absent removes it." } } }, "UpgradeArtifacts": { "type": "object", "description": "Everything the UI needs to upgrade a daemon: the binary steps per OS, and the docker method.", "required": [ "linux", "macos", "windows", "freebsd", "docker", "legacy_service_name" ], "properties": { "docker": { "$ref": "#/components/schemas/DockerUpgrade" }, "freebsd": { "type": "array", "items": { "$ref": "#/components/schemas/UpgradeStep" }, "description": "Binary upgrade steps on FreeBSD." }, "legacy_service_name": { "type": "boolean", "description": "The restart step names the service after the daemon, as installs from before the install\nengine were named. Such a daemon may have been set up differently, so the UI points the\noperator to `scanopy-daemon list` for the real service." }, "linux": { "type": "array", "items": { "$ref": "#/components/schemas/UpgradeStep" }, "description": "Binary upgrade steps on Linux." }, "macos": { "type": "array", "items": { "$ref": "#/components/schemas/UpgradeStep" }, "description": "Binary upgrade steps on macOS." }, "windows": { "type": "array", "items": { "$ref": "#/components/schemas/UpgradeStep" }, "description": "Binary upgrade steps on Windows." } } }, "UpgradeStep": { "type": "object", "description": "One command of a binary upgrade, in the order it runs.", "required": [ "type", "command" ], "properties": { "command": { "type": "string", "description": "The shell command to run." }, "type": { "$ref": "#/components/schemas/UpgradeStepType" } } }, "UpgradeStepType": { "type": "string", "description": "What one upgrade step does. The UI labels each step by its type.", "enum": [ "Download", "Restart", "Stop", "Start" ] }, "UpstreamSupport": { "type": "string", "description": "Whether the vendor publishes and supports the API a credential type talks to.\n\nDeliberately *not* folded into [`CredentialStability`], because the two describe different\nthings and change independently. Stability is about our own maturity and is meant to be retired\nby promotion to `Stable`; an undocumented upstream is a permanent property of the vendor's API\nthat our promotion does not change. Collapsing them would force an integration built on a\nreverse-engineered API to sit in `Beta` forever to keep the warning — or to reach `Stable` with\nthe warning silently dropped. UniFi is the proof that both combinations are real: it is\n`Stable` and `Undocumented` today.", "enum": [ "Vendor", "Undocumented" ] }, "UseCase": { "type": "string", "enum": [ "homelab", "internal_it", "msp", "other" ] }, "User": { "allOf": [ { "$ref": "#/components/schemas/UserBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "UserApiKey": { "allOf": [ { "$ref": "#/components/schemas/UserApiKeyBase" }, { "type": "object", "required": [ "id", "updated_at", "created_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true } } } ] }, "UserApiKeyBase": { "type": "object", "required": [ "key", "name", "user_id", "organization_id", "last_used", "tags" ], "properties": { "expires_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When this record stops being valid." }, "is_enabled": { "type": "boolean", "description": "Whether the key may still be used. Disabled keys are rejected." }, "key": { "type": "string", "description": "The stored key. Returned redacted except on creation and rotation.", "readOnly": true }, "last_used": { "type": [ "string", "null" ], "format": "date-time", "description": "When this key was last used to authenticate.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for this key." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "permissions": { "$ref": "#/components/schemas/UserOrgPermissions", "description": "Role the key is limited to, which cannot exceed the user's own." }, "site_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Site IDs this key has access to (hydrated from junction table)" }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "user_id": { "type": "string", "format": "uuid", "description": "User the key acts on behalf of." } } }, "UserApiKeyOrderField": { "type": "string", "description": "Fields the user API key list can be ordered and grouped by.", "enum": [ "created_at", "name", "updated_at", "permissions", "is_enabled", "last_used", "expires_at" ] }, "UserApiKeyResponse": { "type": "object", "description": "Response for user API key creation/rotation\nContains the full API key record plus the plaintext key (shown only once)", "required": [ "api_key", "key" ], "properties": { "api_key": { "$ref": "#/components/schemas/UserApiKey", "description": "The stored key record." }, "key": { "type": "string", "format": "password", "description": "The plaintext API key - only returned once during creation or rotation", "readOnly": true } } }, "UserBase": { "type": "object", "required": [ "email", "organization_id", "permissions", "site_ids" ], "properties": { "display_settings": { "$ref": "#/components/schemas/DisplaySettings", "description": "Per-user date and time display preferences" }, "email": { "type": "string", "format": "email", "description": "The user's email address, also their login identifier." }, "email_settings": { "$ref": "#/components/schemas/EmailSettings", "description": "Per-user email preferences" }, "email_verified": { "type": "boolean", "description": "Whether the user has verified their email address" }, "has_password": { "type": "boolean", "description": "Whether the user has a password set — computed from password_hash, never stored in DB", "readOnly": true }, "oidc_linked_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the account was linked to the identity provider." }, "oidc_provider": { "type": [ "string", "null" ], "description": "Slug of the identity provider this account signs in through, when linked." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "permissions": { "$ref": "#/components/schemas/UserOrgPermissions", "description": "The user's role within the organization." }, "site_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The sites this entity applies to." }, "terms_acceptance_required": { "type": "boolean", "description": "Whether the user must accept the current terms before the API serves\ntheir session. Computed, never stored: set only on the signed-in user's\nown record (`/api/auth/me`, login, accept), false everywhere else.", "readOnly": true } } }, "UserOrgPermissions": { "type": "string", "enum": [ "Owner", "Admin", "Member", "Viewer" ] }, "Uxy": { "type": "object", "description": "2D unsigned coordinate. Used for node positions and sizes.\nElement node sizes are computed by the frontend (elkjs); the backend\nsets `Uxy::default()` for element nodes.", "required": [ "x", "y" ], "properties": { "x": { "type": "integer", "description": "Horizontal position.", "minimum": 0 }, "y": { "type": "integer", "description": "Vertical position.", "minimum": 0 } } }, "VCenterVirtualization": { "type": "object", "properties": { "vm_id": { "type": [ "string", "null" ], "description": "vCenter managed object ID of the guest." }, "vm_name": { "type": [ "string", "null" ], "description": "Guest name as configured in vCenter." } } }, "VerifyEmailRequest": { "type": "object", "description": "Request to verify email using token", "required": [ "token" ], "properties": { "token": { "type": "string", "description": "Single-use token from the verification email." } } }, "VersionHealthStatus": { "type": "string", "description": "Health status for daemon versions.\n\nLifecycle order: `Current` → `Outdated` → `Deprecated` → `Unsupported`, with\n`Unknown` for daemons whose version the server has no record of.", "enum": [ "Current", "Outdated", "Deprecated", "Unsupported", "Unknown" ] }, "VersionInfo": { "type": "object", "description": "Version information for API compatibility checking", "required": [ "api_version", "server_version" ], "properties": { "api_version": { "type": "integer", "format": "int32", "description": "Current API version (integer, increments on breaking changes)", "minimum": 0 }, "min_compatible_client": { "type": [ "string", "null" ], "description": "Minimum client version that can use this API (optional, for future use)" }, "server_version": { "type": "string", "description": "Server version (semver)", "example": "0.12.10" } } }, "ViewElementConfig": { "type": "object", "description": "Defines the entity hierarchy for a topology view:\ncontainer (grouping box) → element (rendered as nodes) → inline (shown inside nodes)", "required": [ "element_entities" ], "properties": { "collective_noun": { "type": [ "string", "null" ], "description": "Single noun spanning all element entities. Used in summaries when the\nper-entity breakdown would be confusing (e.g. mixed Host+Service in\nWorkloads, where everything is conceptually a \"workload\"). Singular;\nthe UI pluralizes as needed." }, "container_entity": { "oneOf": [ { "$ref": "#/components/schemas/EntityDiscriminants", "description": "Entity rendered as the container/grouping box (e.g. Subnet in L3, Host in L2/Workloads)" }, { "type": "null" } ] }, "default_hidden_values": { "type": "object", "description": "Filter values this view hides out of the box, keyed the same way as the\nhide-set in request options.\n\nThese are product defaults rather than user filters: they keep a view\nlegible before anyone has touched it, so \"clear all filters\" preserves\nthem and the \"filters applied\" count ignores them. Declared here so the\nbackend's initial hide-set and the frontend's notion of what counts as a\ndefault are the same list — they were separately hardcoded before, in\n`default_hide_metadata_values` and again in the options panel.", "additionalProperties": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } }, "propertyNames": { "type": "string", "description": "The type of metadata filter. One variant per conceptually-distinct filter\nacross the app — Category (on Service), Virtualization (on Host), and so\non. Kept narrow on purpose: adding a new filter means adding a variant\nhere + a `HasFilterValues` impl on the relevant entity.", "enum": [ "Category", "Virtualization", "LinkState", "Staleness", "OperStatus", "GuestState" ] } }, "propertyNames": { "type": "string", "enum": [ "Site", "Vlan", "Subnet", "Host", "Service", "IPAddress", "Interface", "Port", "Binding", "Discovery", "Daemon", "DaemonApiKey", "Tag", "TagGroup", "User", "UserApiKey", "Credential", "Organization", "Invite", "Topology", "Snapshot", "Share", "Dependency" ] } }, "element_entities": { "type": "array", "items": { "$ref": "#/components/schemas/ViewElementEntityConfig" }, "description": "Per-element-entity config: the entity types rendered as element nodes and\n(for each) which other entity types are shown *inside* those cards.\nReplaces the old flat `inline_entities` so views like Workloads — where\nHost elements inline services but Service elements inline nothing — can\nbe expressed correctly." }, "element_marks": { "type": "array", "items": { "$ref": "#/components/schemas/ElementMark" }, "description": "The colours this view paints on its element cards, each tied to the filter value that\nexplains it. A card shows a classification colour only through one of these, so every\ncolour has a chip in the filter panel whose hover rings the same cards. Per channel, earlier\nmarks win." }, "metadata_filters": { "type": "array", "items": { "$ref": "#/components/schemas/MetadataFilter" }, "description": "The metadata filters this view offers, each naming the entities it reads and hides.\nApplied regardless of an entity's role (container/element/inline): a Service filter\nrenders under the Services section whether Service is an element entity\n(Workloads/Application) or an inline entity (L3). A filter naming several entities renders\nin a section of its own." } } }, "ViewElementEntityConfig": { "type": "object", "description": "An element-entity slot inside a view, plus the set of entity types that\nrender inline on that element's card. Card rendering (what services/ports\nshow up inside an element) and layout re-trigger fingerprinting both read\nfrom this list.", "required": [ "entity_type", "inline_entities" ], "properties": { "entity_type": { "$ref": "#/components/schemas/EntityDiscriminants", "description": "Entity type drawn as an element in this view." }, "inline_entities": { "type": "array", "items": { "$ref": "#/components/schemas/EntityDiscriminants" }, "description": "Entity types folded into that element rather than drawn separately." } } }, "ViewElementOrder": { "type": "string", "description": "How elements inside a container are ordered before layout.", "enum": [ "automatic", "oper_status" ] }, "ViewHandleStrategy": { "type": "string", "description": "How edge endpoints pick the side of a node they attach to.", "enum": [ "horizontal", "optimal" ] }, "ViewInspectorConfig": { "type": "object", "description": "View-specific inspector panel configuration.\nDetermines which sections appear and in what order for each view.", "required": [ "element_sections", "container_sections", "show_application_picker", "show_service_network_detail" ], "properties": { "container_sections": { "type": "array", "items": { "$ref": "#/components/schemas/InspectorSection" }, "description": "Inspector sections shown when a container is selected." }, "dependency_creation": { "oneOf": [ { "$ref": "#/components/schemas/DependencyMemberType", "description": "What type of member a dependency can be drawn between in this view." }, { "type": "null" } ] }, "element_sections": { "type": "array", "items": { "$ref": "#/components/schemas/InspectorSection" }, "description": "Inspector sections shown when an element is selected." }, "show_application_picker": { "type": "boolean", "description": "Whether the inspector offers the application picker." }, "show_service_network_detail": { "type": "boolean", "description": "Whether a service card shows the IP address and ports it is reached on. Application\ngroups services by what they do, not where they listen, so it leaves these out." } } }, "ViewLayoutAlgorithm": { "type": "string", "description": "Root-level layout algorithm for a view.", "enum": [ "layered_right", "workloads", "rect_packing", "default" ] }, "ViewLayoutConfig": { "type": "object", "description": "How the UI lays out a view. Read by the topology layout pipeline in place of branching on\nthe view id.", "required": [ "algorithm", "handle_strategy", "element_order", "root_order", "root_edges", "repack_disconnected", "align_layers_top", "force_overview" ], "properties": { "algorithm": { "$ref": "#/components/schemas/ViewLayoutAlgorithm" }, "align_layers_top": { "type": "boolean", "description": "Whether each column of root containers is aligned to the same top edge after layout." }, "element_order": { "$ref": "#/components/schemas/ViewElementOrder" }, "force_overview": { "type": "boolean", "description": "Whether the view switches to a force-directed overview when every root container is\ncollapsed." }, "handle_strategy": { "$ref": "#/components/schemas/ViewHandleStrategy" }, "repack_disconnected": { "type": "boolean", "description": "Whether root containers with no cross-container edges are moved beside the connected\nones after layout." }, "root_edges": { "type": "boolean", "description": "Whether edges between root containers are passed to the root layout." }, "root_order": { "$ref": "#/components/schemas/ViewRootOrder" } } }, "ViewRootOrder": { "type": "string", "description": "How root containers are ordered before layout.", "enum": [ "input", "workload_count" ] }, "Vlan": { "allOf": [ { "$ref": "#/components/schemas/VlanBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created.", "readOnly": true }, "first_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The discovery that first observed this entity.", "readOnly": true }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier.", "readOnly": true }, "last_discovery_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The most recent discovery that observed this entity.", "readOnly": true }, "last_seen_at": { "type": "string", "format": "date-time", "description": "When a discovery last observed this entity.", "readOnly": true }, "lineage_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Stable identifier shared by every revision of the same entity across its history.", "readOnly": true }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified.", "readOnly": true }, "valid_from": { "type": "string", "format": "date-time", "description": "Start of the interval this revision was current for (SCD2 history).", "readOnly": true }, "valid_to": { "type": [ "string", "null" ], "format": "date-time", "description": "End of the interval this revision was current for. `null` while it is the live revision.", "readOnly": true } } } ] }, "VlanBase": { "allOf": [ { "$ref": "#/components/schemas/VlanName", "description": "Human-facing name for this VLAN, with the source that named it.\n\nSerialises as the two flat keys `name` and `name_source`, so a client that sends only\n`name` still deserialises, at `Unspecified`." }, { "type": "object", "required": [ "vlan_number", "site_id", "organization_id", "tags" ], "properties": { "description": { "type": [ "string", "null" ], "description": "Free-text notes about the VLAN." }, "interface_ids": { "type": "array", "items": { "type": "string", "format": "uuid", "readOnly": true }, "description": "Interfaces that serve this VLAN, as their native VLAN or one of their\nmember VLANs (`Interface::native_vlan_id` / `Interface::vlan_ids`).\nHydrated by `VlanService` on read; not a column on `vlans`." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "How this VLAN came to be known — discovered, imported, or created by hand." }, "subnet_ids": { "type": "array", "items": { "type": "string", "format": "uuid", "readOnly": true }, "description": "Subnets associated with this VLAN, derived from discovered interface\nnative-VLAN data via the `subnet_vlans` junction. Hydrated by\n`VlanService` on read; it is not a column on `vlans`, so anything sent\nhere on create/update is ignored by `to_params`." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "vlan_number": { "type": "integer", "format": "int32", "description": "The 802.1Q VLAN number (1-4094)", "minimum": 0 } } } ] }, "VlanDiscoveryItem": { "type": "object", "required": [ "vlan_number", "name" ], "properties": { "name": { "type": "string", "description": "VLAN name as configured on the device." }, "name_source": { "$ref": "#/components/schemas/AttributeSource", "description": "Where the daemon read the name. Defaults to an SNMP read: daemons that predate this field\nsent VLANs from SNMP alone." }, "vlan_number": { "type": "integer", "format": "int32", "description": "802.1Q VLAN ID.", "minimum": 0 } } }, "VlanDiscoveryRequest": { "type": "object", "description": "Request body for daemon VLAN discovery upsert", "required": [ "site_id", "vlans" ], "properties": { "site_id": { "type": "string", "format": "uuid", "description": "The site this entity belongs to." }, "vlans": { "type": "array", "items": { "$ref": "#/components/schemas/VlanDiscoveryItem" }, "description": "VLANs observed by the daemon." } } }, "VlanDiscoveryResponse": { "type": "object", "description": "Response for discovery upsert", "required": [ "vlans" ], "properties": { "vlans": { "type": "array", "items": { "$ref": "#/components/schemas/VlanDiscoveryResponseItem" }, "description": "Mapping of vlan_number → VLAN entity UUID" } } }, "VlanDiscoveryResponseItem": { "type": "object", "required": [ "vlan_number", "id" ], "properties": { "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "vlan_number": { "type": "integer", "format": "int32", "description": "802.1Q VLAN ID.", "minimum": 0 } } }, "VlanName": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "Human-facing name for this VLAN." }, "name_source": { "$ref": "#/components/schemas/AttributeSource" } } }, "VlanOrderField": { "type": "string", "enum": [ "created_at", "name", "vlan_number", "updated_at", "site_id", "source", "last_seen_at" ] }, "WakeOnLanResult": { "type": "object", "description": "Whether one address woke: whether the scan found it after the magic packets went out.", "required": [ "ip", "woke" ], "properties": { "ip": { "type": "string", "description": "The address the magic packets were sent for." }, "woke": { "type": "boolean", "description": "Whether the scan found the address after the packets went out." } } }, "WarningRemedy": { "type": "string", "description": "What a warning asks of the person reading it.\n\nThe rung a code sits on *is* its instruction, which is why the warning list is grouped by this\nand not by [`Severity`]: a reader scanning a completed run needs to know whether they are being\nasked for something before they need to know how much data was lost. It reaches the UI as each\ncode's `category`, with this enum's own registry supplying the heading — the same shape\n`service-definitions.json` and `service-categories.json` use.\n\nDeclaration order is the order the sections render in, most demanding first.", "enum": [ "FixInScanopy", "CheckTheDevice", "ClearsOnTheNextScan", "NothingToDo" ] }, "WeekStart": { "type": "string", "enum": [ "monday", "sunday" ] } }, "securitySchemes": { "daemon_api_key": { "type": "apiKey", "in": "header", "name": "Authorization", "description": "Daemon API key (Bearer scp_d_...). Requires X-Daemon-ID header." }, "session": { "type": "apiKey", "in": "cookie", "name": "session_id", "description": "Browser session cookie. Obtained via /api/auth/login." }, "user_api_key": { "type": "apiKey", "in": "header", "name": "Authorization", "description": "User API key (Bearer scp_u_...). Create in Platform > API Keys." } } }, "tags": [ { "name": "Bindings", "description": "Service bindings linking services to IP addresses and/or ports. Defines where a service is accessible." }, { "name": "Daemons", "description": "Daemons are scanning agents that connect to the server to perform network discovery." }, { "name": "Daemon API Keys", "description": "API keys for daemon authentication. Create and manage keys that allow daemons to communicate with the server." }, { "name": "Discoveries", "description": "Network discovery operations. Trigger and monitor scans that detect hosts, services, and network topology." }, { "name": "Dependencies", "description": "Service dependency relationships. Define how services depend on each other." }, { "name": "Hosts", "description": "Network hosts (devices). Manage discovered or manually created hosts on your network." }, { "name": "Interfaces", "description": "SNMP ifTable entries. Physical and logical interfaces discovered via SNMP on hosts." }, { "name": "IP Addresses", "description": "IP addresses assigned to hosts. Each address belongs to a host and a subnet, optionally has a MAC address, and represents an observed or configured address on the network." }, { "name": "Invites", "description": "Organization invitations. Invite users to join your organization." }, { "name": "Sites", "description": "Site containers. Top-level organizational unit that contains subnets, hosts, and other entities." }, { "name": "Organizations", "description": "Manage organization settings." }, { "name": "Ports", "description": "Ports that have been scanned and found open on a host." }, { "name": "Services", "description": "Services running on hosts. Detected or manually added services like databases, web servers, etc." }, { "name": "Shares", "description": "Shared network views. Create read-only shareable links to your network topology." }, { "name": "Snapshots", "description": "Point-in-time capture of a site's topology and entities. Created manually via the topology tab; loadable from the snapshots dropdown." }, { "name": "Credentials", "description": "Credentials the daemon uses to collect from integrations during discovery." }, { "name": "Subnets", "description": "IP subnets within sites. Define address ranges and organize hosts by subnet." }, { "name": "Tags", "description": "Custom tags for categorization. Apply labels to entities for filtering and organization." }, { "name": "Tag Groups", "description": "Sets of tags an entity holds one of, in a set order, optionally limited to some entity types." }, { "name": "Topologies", "description": "Network topology maps showing host relationships and connections." }, { "name": "Users", "description": "User account management. Manage user profiles and permissions within organizations." }, { "name": "User API Keys", "description": "User API keys for programmatic access. Create and manage personal API keys with scoped permissions." }, { "name": "Vlans", "description": "VLANs (802.1Q virtual LANs) defined or discovered on the network. Each VLAN has a number (1-4094), a name, and an optional description, and is referenced by interfaces that participate in it." }, { "name": "auth", "description": "Authentication and session management. Handle user login, logout, and session state." }, { "name": "billing", "description": "Subscription, plan and payment management for the organization." }, { "name": "config", "description": "Server configuration. Public configuration settings for client applications." }, { "name": "dashboard", "description": "Aggregate counts and recent activity for the landing view." }, { "name": "deprecated", "description": "Superseded endpoints, still served for older clients. Avoid in new integrations." }, { "name": "github", "description": "GitHub integration endpoints." }, { "name": "internal", "description": "Internal endpoints for system operations. Not part of the public API." }, { "name": "metadata", "description": "Entity metadata registry. Schema information for all entity types in the system." }, { "name": "release-notes", "description": "Release notes for the version this server runs, and which of them the caller has seen." }, { "name": "search", "description": "Search every entity type the caller can list, by text and tags." }, { "name": "system", "description": "System information endpoints. Version and compatibility checking." } ] }