{ "openapi": "3.1.0", "info": { "title": "Scanopy API", "description": "\nNetwork 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.17.13\"\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.17.13\",\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.17.13)**, 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 **network(s)**:\n\n- You can only access entities within your organization\n- Network-level entities (hosts, services, etc.) are filtered to networks you have access to\n- Use `?network_id=` to filter list endpoints to a specific network\n- API keys can be scoped to a subset of your accessible networks\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/check-email": { "post": { "tags": [ "auth", "internal" ], "operationId": "check_email", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckEmailRequest" } } }, "required": true }, "responses": { "200": { "description": "Email is available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "409": { "description": "Email already in use", "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": "Verification email sent", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Invalid request or already verified", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "429": { "description": "Rate limited", "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, networks, seed preference) 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/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/inquiry": { "post": { "tags": [ "billing", "internal" ], "summary": "Submit enterprise plan inquiry", "description": "Updates Brevo contact/company with inquiry data, creates a deal, and\ntracks an event for automation triggers. Requires authentication to\nlink the inquiry to an organization.", "operationId": "submit_enterprise_inquiry", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EnterpriseInquiryRequest" } } }, "required": true }, "responses": { "200": { "description": "Inquiry submitted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "400": { "description": "Invalid request or Brevo not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "401": { "description": "Authentication required", "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/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/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/github-stars": { "get": { "tags": [ "github", "internal" ], "summary": "Get GitHub star count", "description": "Returns the current star count for the Scanopy GitHub repository.", "operationId": "get_stars", "responses": { "200": { "description": "GitHub star count", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_u32" } } } } } } }, "/api/v1/auth/daemon": { "get": { "tags": [ "Daemon API Keys" ], "summary": "List all Daemon API Keys", "operationId": "list_daemon_api_keys", "parameters": [ { "name": "network_id", "in": "query", "description": "Filter by network 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 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": "network_id", "in": "query", "description": "Filter by network 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 Daemon API Keys", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "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": "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 network 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": "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/{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": "network_id", "in": "query", "description": "Filter by network 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": "network_id", "in": "query", "description": "Filter by network 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`).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CredentialTypeDiscriminants" } ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CredentialOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CredentialOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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`).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CredentialTypeDiscriminants" } ] } }, { "name": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CredentialOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/CredentialOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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": "network_id", "in": "query", "description": "Filter by network 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": [ { "type": "null" }, { "$ref": "#/components/schemas/DaemonOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DaemonOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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/email-install-command": { "post": { "tags": [ "Daemons" ], "summary": "Email install command to current user", "description": "Session-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", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "403": { "description": "User session required", "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": "network_id", "in": "query", "description": "Filter by network 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": [ { "type": "null" }, { "$ref": "#/components/schemas/DaemonOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DaemonOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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/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/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 (network, 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`\ncommand 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 `install` — whether the daemon has checked in (`last_seen`) to decide between\na first-install and a minimal re-key command.", "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` (with the api-key placeholder) or `reconfigure` (credential-free).", "required": true, "schema": { "$ref": "#/components/schemas/InstallCommandKind" } }, { "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/dashboard/summary": { "get": { "tags": [ "dashboard", "internal" ], "summary": "Get dashboard summary", "description": "Returns aggregated dashboard data including network 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": "network_id", "in": "query", "description": "Filter by network 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": [ { "type": "null" }, { "$ref": "#/components/schemas/DependencyOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DependencyOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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": "network_id", "in": "query", "description": "Filter by network 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": [ { "type": "null" }, { "$ref": "#/components/schemas/DependencyOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DependencyOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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": "network_id", "in": "query", "description": "Filter by network ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "daemon_id", "in": "query", "description": "Filter by daemon ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "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": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DiscoveryOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DiscoveryOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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-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/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": "network_id", "in": "query", "description": "Filter by network ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "daemon_id", "in": "query", "description": "Filter by daemon ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "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": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DiscoveryOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DiscoveryOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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/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 those and get a much 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": "network_id", "in": "query", "description": "Filter by network 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": "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": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the host's\nname, hostname and description, and against its IP addresses and the\nnames of services running on it.", "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": [ { "type": "null" }, { "$ref": "#/components/schemas/HostOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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\nnetwork's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the host's own network's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "include_children", "in": "query", "description": "`false` returns hosts with empty `ip_addresses`/`ports`/`services`/\n`interfaces`. 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: network not found, subnet mismatch, or invalid tags", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "401": { "description": "No access to the specified network", "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/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 networks", "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": "network_id", "in": "query", "description": "Filter by network 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": "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": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the host's\nname, hostname and description, and against its IP addresses and the\nnames of services running on it.", "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": [ { "type": "null" }, { "$ref": "#/components/schemas/HostOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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\nnetwork's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the host's own network's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "include_children", "in": "query", "description": "`false` returns hosts with empty `ip_addresses`/`ports`/`services`/\n`interfaces`. 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": "network_id", "in": "query", "description": "Filter by network 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": "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": "search", "in": "query", "description": "Free-text search. Case-insensitive substring match against the host's\nname, hostname and description, and against its IP addresses and the\nnames of services running on it.", "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": [ { "type": "null" }, { "$ref": "#/components/schemas/HostOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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\nnetwork's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the host's own network's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } }, { "name": "include_children", "in": "query", "description": "`false` returns hosts with empty `ip_addresses`/`ports`/`services`/\n`interfaces`. 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/{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 network.\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 networks", "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)\nare managed via their own endpoints.\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 — evidence it can\nreach the address — and only if that daemon still has an interface on a\nsubnet containing one of the host's scannable IPs. Where that interface has a\nMAC the daemon ARPs the target, which sees a live host even when every port\nis firewalled; on a MAC-less interface (a point-to-point tunnel) it falls\nback to a TCP probe. When no interface covers any of the host's addresses the\nrequest is refused with the specific reason. A loopback address is not a\nscannable IP — it is reached locally and is excluded from the target set.\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, daemon unreachable, 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/if-entries": { "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": "network_id", "in": "query", "description": "Filter by network 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_if_entry", "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": "Network mismatch or duplicate if_index", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/if-entries/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/if-entries/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": "network_id", "in": "query", "description": "Filter by network 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/if-entries/{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_if_entry", "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": "Network 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/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/{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": "network_id", "in": "query", "description": "Filter by network 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": "Network 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": "network_id", "in": "query", "description": "Filter by network 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": "Network 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/networks": { "get": { "tags": [ "Networks" ], "summary": "List all networks", "operationId": "get_all_networks", "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 networks", "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/NetworkBase" }, { "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 Network", "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": [ "Networks" ], "summary": "Create a new network", "operationId": "create_network", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Network" } } }, "required": true }, "responses": { "200": { "description": "Network created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Network" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/networks/bulk-delete": { "post": { "tags": [ "Networks" ], "summary": "Bulk delete networks", "operationId": "bulk_delete_networks", "requestBody": { "description": "Array of Network IDs to delete", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string", "format": "uuid" } } } }, "required": true }, "responses": { "200": { "description": "Networks 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/networks/export/csv": { "get": { "tags": [ "Networks" ], "summary": "Export Networks to CSV", "description": "Export all Networks matching the filter criteria to CSV format. Ignores pagination parameters (limit/offset) and exports all matching records.", "operationId": "export_networks_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 Networks", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/networks/{id}": { "get": { "tags": [ "Networks" ], "summary": "Get a network by ID", "operationId": "get_by_id_network", "parameters": [ { "name": "id", "in": "path", "description": "Network ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Network found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Network" } } } }, "404": { "description": "Network not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "put": { "tags": [ "Networks" ], "summary": "Update a network", "operationId": "update_network", "parameters": [ { "name": "id", "in": "path", "description": "Network ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Network" } } }, "required": true }, "responses": { "200": { "description": "Network updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse_Network" } } } }, "403": { "description": "User not admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Network not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] }, "delete": { "tags": [ "Networks" ], "summary": "Delete a network", "operationId": "delete_network", "parameters": [ { "name": "id", "in": "path", "description": "Network ID", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Network deleted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } }, "403": { "description": "User not admin", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Network not found", "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 persists a distinct onboarding milestone (the org subscriber dedups); the\nPostHog subscriber turns these into funnel events, so no 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": "network_id", "in": "query", "description": "Filter by network 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": "Network 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": "network_id", "in": "query", "description": "Filter by network 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": "Network 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/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": "network_id", "in": "query", "description": "Filter by network ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "host_id", "in": "query", "description": "Filter by host 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": "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": "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": [ { "type": "null" }, { "$ref": "#/components/schemas/ServiceOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ServiceOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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\nnetwork's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the service's own network'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 `network_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 network 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": "network_id", "in": "query", "description": "Filter by network ID", "required": false, "schema": { "type": [ "string", "null" ], "format": "uuid" } }, { "name": "host_id", "in": "query", "description": "Filter by host 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": "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": "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": [ { "type": "null" }, { "$ref": "#/components/schemas/ServiceOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ServiceOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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\nnetwork's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the service's own network'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/{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 network 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": "network_id", "in": "query", "description": "Filter by network 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": "network_id", "in": "query", "description": "Filter by network 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" } } } }, "404": { "description": "Share not found", "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" } } } }, "401": { "description": "Invalid password", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiErrorResponse" } } } }, "404": { "description": "Share not found", "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/snapshots": { "get": { "tags": [ "Snapshots" ], "summary": "List all Snapshots", "operationId": "list_snapshots", "parameters": [ { "name": "network_id", "in": "query", "description": "Filter by network 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 network.\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": "Network 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 network.\nSupports pagination via `limit` and `offset` query parameters,\nand ordering via `group_by`, `order_by`, and `order_direction`.", "operationId": "list_subnets", "parameters": [ { "name": "network_id", "in": "query", "description": "Filter by network 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": [ { "type": "null" }, { "$ref": "#/components/schemas/SubnetOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SubnetOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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\nnetwork's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the subnet's own network's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "List of subnets", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedApiResponse_Subnet" } } } } }, "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": "network_id", "in": "query", "description": "Filter by network 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": [ { "type": "null" }, { "$ref": "#/components/schemas/SubnetOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SubnetOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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\nnetwork's staleness window; `false` returns only those it has. Omit for\nboth. Evaluated per row against the subnet's own network's window.", "required": false, "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "CSV file containing Subnets", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "security": [ { "user_api_key": [] }, { "session": [] } ] } }, "/api/v1/subnets/{id}": { "get": { "tags": [ "Subnets" ], "summary": "Get Subnet by ID", "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_Subnet" } } } }, "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/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": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/TagOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/TagOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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, Group, Network, Discovery, Daemon, DaemonApiKey, UserApiKey)\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, Group, Network, Discovery, Daemon, DaemonApiKey, UserApiKey)\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, Group, Network, Discovery, Daemon, DaemonApiKey, UserApiKey)\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": "group_by", "in": "query", "description": "Primary ordering field (used for grouping). Always sorts ASC to keep groups together.", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/TagOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/TagOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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/{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 networks.", "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": "network_id", "in": "query", "description": "Filter by network 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": "network_id", "in": "query", "description": "Network 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" ] } } ], "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": "network_id", "in": "query", "description": "Filter by network 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" } } } }, "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" } } } }, "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 users with higher permissions", "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/{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 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" } } } }, "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 network.", "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": [ { "type": "null" }, { "$ref": "#/components/schemas/VlanOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/VlanOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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": "network_id", "in": "query", "description": "Filter by network ID", "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" } } ], "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 network. VLAN numbers must be unique within a network.", "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 network", "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": [ { "type": "null" }, { "$ref": "#/components/schemas/VlanOrderField" } ] } }, { "name": "order_by", "in": "query", "description": "Secondary ordering field (sorting within groups or standalone sort).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/VlanOrderField" } ] } }, { "name": "order_direction", "in": "query", "description": "Direction for order_by field (group_by always uses ASC).", "required": false, "schema": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OrderDirection" } ] } }, { "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": "network_id", "in": "query", "description": "Filter by network ID", "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" } } ], "responses": { "200": { "description": "CSV file containing Vlans", "content": { "text/csv": { "schema": { "type": "string" } } } } }, "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.17.13" } }, "example": { "api_version": 1, "server_version": "0.17.13" } }, "ApiResponse": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/TupleUnit", "description": "The result payload. Omitted on failure." } ], "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Binding": { "type": "object", "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-08-26T01:22:52.448346Z", "first_discovery_id": null, "id": "54dc1afe-6180-42ef-9772-f9e4329207ad", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-08-26T01:22:52.448346Z", "lineage_id": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "type": "Port", "updated_at": "2026-08-26T01:22:52.448346Z", "valid_from": "2026-08-26T01:22:52.448346Z", "valid_to": null } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_BulkDeleteResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_BulkTagResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_CancelSubscriptionResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_ChangePlanPreview": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The result payload. Omitted on failure.", "required": [ "excess_hosts", "excess_networks", "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_networks": { "type": "integer", "format": "int64", "description": "Networks over the target plan's allowance.", "minimum": 0 }, "excess_seats": { "type": "integer", "format": "int64", "description": "Seats over the target plan's allowance.", "minimum": 0 } } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Credential": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/CredentialBase" }, { "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Daemon": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_DaemonApiKey": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_DaemonApiKeyResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_DaemonRegistrationResponse": { "type": "object", "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": [ { "type": "null" }, { "$ref": "#/components/schemas/ServerCapabilities", "description": "Server capabilities (returned if daemon sends version info)" } ] } } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_DaemonResponse": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/DaemonBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "version_status", "interfaced_subnet_ids" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_DashboardSummary": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Dashboard summary response", "required": [ "networks", "daemons", "recent_discoveries", "plan_usage" ], "properties": { "daemons": { "type": "array", "items": { "$ref": "#/components/schemas/DaemonResponse" }, "description": "Daemons the caller can see, with their current status." }, "networks": { "type": "array", "items": { "$ref": "#/components/schemas/NetworkSummary" }, "description": "Per-network counts for every network the caller can see." }, "plan_usage": { "$ref": "#/components/schemas/PlanUsage", "description": "Current usage against the organization's plan allowances." }, "recent_discoveries": { "type": "array", "items": { "$ref": "#/components/schemas/Discovery" }, "description": "The most recent discovery runs, newest first." } } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_DemoPopulateStatus": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Dependency": { "type": "object", "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", "network_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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Discovery": { "type": "object", "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, `network_credentials` for a broadcast one (see\n[`Discovery::take_network_scope_credential_ids`])." }, "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_DiscoveryUpdatePayload": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Progress update from daemon to server during discovery", "required": [ "session_id", "daemon_id", "network_id", "phase", "discovery_type", "progress" ], "properties": { "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon this entity refers to." }, "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 kind 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 }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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 }, "scanned": { "oneOf": [ { "type": "null" }, { "$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." } ] }, "session_id": { "type": "string", "format": "uuid", "description": "The discovery run this update 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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_HostResponse": { "type": "object", "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", "network_id", "source", "hidden", "tags", "ip_addresses", "ports", "services", "interfaces" ], "properties": { "chassis_id": { "type": [ "string", "null" ], "description": "LLDP chassis identifier, used to match the host to its neighbours." }, "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." }, "hidden": { "type": "boolean", "description": "Whether the host is hidden from topology views." }, "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", "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_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 rest of the SCD2/audit columns are not." }, "management_url": { "type": [ "string", "null" ], "description": "Link to the host's own management interface." }, "manufacturer": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalMfgName — hardware manufacturer. Read-only, as above.", "readOnly": true }, "model": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalModelName — hardware model. Read-only, as above.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for the host." }, "name_source": { "$ref": "#/components/schemas/HostNameSource", "description": "Which rung of the naming ladder produced `name`. Read-only: it is decided by whoever\nsupplied the name, not by the caller." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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 }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Services running on this host." }, "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_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_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_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." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "virtualization_metadata": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } }, "example": { "created_at": "2026-01-15T10:30:00Z", "credential_assignments": [], "description": "Primary web server", "hidden": false, "hostname": "web-server-01.local", "id": "550e8400-e29b-41d4-a716-446655440003", "interfaces": [ { "admin_status": "Up", "cdp_address": null, "cdp_device_id": null, "cdp_platform": null, "cdp_port_id": null, "created_at": "2026-01-15T10:30:00Z", "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, "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "lldp_chassis_id": null, "lldp_mgmt_addr": null, "lldp_port_desc": null, "lldp_port_id": null, "lldp_sys_desc": null, "lldp_sys_name": null, "mac_address": "DE:AD:BE:EF:CA:FE", "neighbor": null, "neighbor_seen_at": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "oper_status": "Up", "speed_bps": 1000000000, "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "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", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "name": "eth0", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "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_seen_at": "2026-01-15T10:30:00Z", "name": "web-server-01", "name_source": "Manual", "network_id": "550e8400-e29b-41d4-a716-446655440002", "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, "network_id": "550e8400-e29b-41d4-a716-446655440002", "number": 80, "protocol": "Tcp", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "services": [ { "bindings": [ { "created_at": "2026-08-26T01:22:52.422761Z", "first_discovery_id": null, "id": "75ac6952-cc37-4a61-a171-ce1ad0dde536", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-08-26T01:22:52.422761Z", "lineage_id": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "type": "Port", "updated_at": "2026-08-26T01:22:52.422761Z", "valid_from": "2026-08-26T01:22:52.422761Z", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "service_definition": "Jotty", "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 } ], "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "virtualization_metadata": null, "virtualization_service_id": null } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_IPAddress": { "type": "object", "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", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "name": "eth0", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_InstallArtifacts": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Interface": { "type": "object", "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 }, "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Invite": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Network": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/NetworkBase" }, { "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 Network", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "stale_after_hours": null, "tags": [], "updated_at": "2026-01-15T10:30:00Z" } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_OnboardingStateResponse": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response from onboarding state endpoint", "properties": { "network": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OnboardingNetworkState", "description": "Network from pending setup (with name and ID)" } ] }, "network_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Network ID from pending setup (if any)" }, "org_name": { "type": [ "string", "null" ], "description": "Organization name from pending setup" }, "step": { "type": [ "string", "null" ], "description": "Current onboarding step (if any)" }, "use_case": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/UseCase", "description": "Use case selection (homelab, company, msp)" } ] } } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Option_SaveOfferCoupon": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Organization": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Port": { "type": "object", "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, "network_id": "550e8400-e29b-41d4-a716-446655440002", "number": 80, "protocol": "Tcp", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_ProvisionDaemonResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_PublicConfigResponse": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "The result payload. Omitted on failure.", "required": [ "server_port", "disable_registration", "disable_password_login", "oidc_providers", "billing_enabled", "discount_save_offer_available", "has_integrated_daemon", "has_email_service", "has_email_opt_in", "public_url", "needs_cookie_consent", "deployment_type", "license_in_grace_period", "org_limit_reached", "server_admin_contact_email" ], "properties": { "billing_enabled": { "type": "boolean", "description": "Whether this deployment has billing configured." }, "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_opt_in": { "type": "boolean", "description": "Whether the deployment asks users to opt in to product email." }, "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_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_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_status": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/LicenseStatusDiscriminants", "description": "Runtime state of the configured license key. `None` on deployments\nthat don't require one (community and cloud)." } ] }, "needs_cookie_consent": { "type": "boolean", "description": "Whether the client should show a cookie-consent prompt." }, "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_PublicShareMetadata": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_ServerCapabilities": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Server capabilities returned on startup/registration", "required": [ "server_version", "minimum_daemon_version" ], "properties": { "deprecation_warnings": { "type": "array", "items": { "$ref": "#/components/schemas/DeprecationWarning" }, "description": "Deprecation warnings for the daemon" }, "minimum_daemon_version": { "type": "string", "description": "Minimum daemon version supported by this server" }, "server_version": { "type": "string", "description": "Server software version" } } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Service": { "type": "object", "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-08-26T01:22:52.442075Z", "first_discovery_id": null, "id": "a3791a62-6392-41d3-849d-a099a9c4ab8a", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-08-26T01:22:52.442075Z", "lineage_id": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "type": "Port", "updated_at": "2026-08-26T01:22:52.442075Z", "valid_from": "2026-08-26T01:22:52.442075Z", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "service_definition": "Jotty", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_SetupIntentResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_SetupResponse": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "object", "description": "Response from setup endpoint", "required": [ "network_id" ], "properties": { "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." } } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Share": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_ShareAccessTokenResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Snapshot": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_String": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "string", "description": "The result payload. Omitted on failure." }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Subnet": { "type": "object", "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", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "subnet_type": "Lan", "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_service_id": null } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Tag": { "type": "object", "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", "id": "550e8400-e29b-41d4-a716-44665544000a", "is_application": false, "lineage_id": null, "name": "production", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_TestReachabilityResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Topology": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_TopologyData": { "type": "object", "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 network's grouping options\n(`build_all_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" ], "properties": { "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" ] } }, "hosts": { "type": "array", "items": { "$ref": "#/components/schemas/Host" }, "description": "Hosts included in this topology." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/Interface" }, "description": "Interfaces included in this topology." }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "IP addresses included in this topology." }, "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/Service" }, "description": "Services included in this topology." }, "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_User": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_UserApiKey": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_UserApiKeyResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Vec_BillingPlan": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Vec_Credential": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "allOf": [ { "$ref": "#/components/schemas/CredentialBase" }, { "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Vec_DiscoveryUpdatePayload": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "array", "items": { "type": "object", "description": "Progress update from daemon to server during discovery", "required": [ "session_id", "daemon_id", "network_id", "phase", "discovery_type", "progress" ], "properties": { "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon this entity refers to." }, "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 kind 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 }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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 }, "scanned": { "oneOf": [ { "type": "null" }, { "$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." } ] }, "session_id": { "type": "string", "format": "uuid", "description": "The discovery run this update 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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Vec_Invite": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_VersionInfo": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_Vlan": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_VlanDiscoveryResponse": { "type": "object", "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": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "ApiResponse_u32": { "type": "object", "required": [ "success", "meta" ], "properties": { "data": { "type": "integer", "format": "int32", "description": "The result payload. Omitted on failure.", "minimum": 0 }, "error": { "type": [ "string", "null" ], "description": "Human-readable failure message. Omitted on success." }, "meta": { "$ref": "#/components/schemas/ApiMeta", "description": "API and server version metadata." }, "success": { "type": "boolean", "description": "`true` when the request succeeded. `false` responses carry `error` instead of `data`." } } }, "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-08-26T01:22:52.423032Z", "first_discovery_id": null, "id": "533ef92f-c97f-4493-abb9-b6e8f37aa9a2", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-08-26T01:22:52.423032Z", "lineage_id": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "type": "Port", "updated_at": "2026-08-26T01:22:52.423032Z", "valid_from": "2026-08-26T01:22:52.423032Z", "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", "network_id" ], "properties": { "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "service_id": { "type": "string", "format": "uuid", "description": "The service this entity refers 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 } } }, "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": [ { "type": "null" }, { "$ref": "#/components/schemas/SaveOffer", "description": "Whether the customer accepted the retention discount instead of cancelling." } ] }, "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." } } }, "ChangePlanPreview": { "type": "object", "required": [ "excess_hosts", "excess_networks", "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_networks": { "type": "integer", "format": "int64", "description": "Networks over the target plan's allowance.", "minimum": 0 }, "excess_seats": { "type": "integer", "format": "int64", "description": "Seats 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." } } }, "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" ] }, "Color": { "type": "string", "enum": [ "Pink", "Rose", "Red", "Amber", "Orange", "Green", "Emerald", "Teal", "Cyan", "Blue", "Indigo", "Purple", "Fuchsia", "Violet", "Sky", "Gray", "Lime", "Yellow" ] }, "ContainerType": { "type": "string", "enum": [ "Subnet", "ServiceCategory", "Application", "ApplicationUngrouped", "Root", "Host", "NestedTag", "NestedServiceCategory", "Hypervisor", "ContainerRuntime", "Stack", "TrunkPort", "VLAN", "PortOpStatus" ] }, "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 `network_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`, `network_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", "network_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." }, "hidden": { "type": "boolean", "description": "Hide the host from topology views without deleting it." }, "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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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)" }, "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_metadata": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } }, "example": { "credential_assignments": [], "description": "Primary web server", "hidden": false, "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "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": "Jotty", "tags": [], "virtualization_metadata": null, "virtualization_service_id": null } ], "tags": [], "virtualization_metadata": null, "virtualization_service_id": null } }, "CreateInviteRequest": { "type": "object", "required": [ "permissions", "network_ids" ], "properties": { "expiration_hours": { "type": [ "integer", "null" ], "format": "int64", "description": "How long the invite stays valid, in hours." }, "network_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The networks this entity applies to." }, "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." } } }, "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 `network_id` to all bindings.", "required": [ "host_id", "network_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 `network_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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "service_definition": { "type": "string", "description": "Which known software this service is, if identified." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "virtualization_metadata": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ServiceVirtualization", "description": "Container identity (name, id, compose project), when it is containerized." } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The container runtime service hosting this container, if any." } } }, "CreateSnapshotRequest": { "type": "object", "required": [ "network_id" ], "properties": { "network_id": { "type": "string", "format": "uuid", "description": "The network 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" ], "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 } } } ] }, "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", "detail" ], "properties": { "address": { "type": "string", "description": "The address the credential was tried against." }, "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", "credential_type", "tags", "assigned_network_ids", "host_assignments" ], "properties": { "assigned_network_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Networks this credential is assigned to (Broadcast scope).\nHydrated from the `network_credentials` junction table." }, "credential_type": { "$ref": "#/components/schemas/CredentialType", "description": "Protocol this credential authenticates with, and its settings." }, "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." } } }, "CredentialOrderField": { "type": "string", "enum": [ "created_at", "name", "updated_at" ] }, "CredentialQueryPayloadDiscriminants": { "type": "string", "enum": [ "Snmp", "DockerProxy", "DockerSocket", "PodmanProxy", "PodmanSocket", "UnifiController", "InstantOn", "Unknown" ] }, "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 `minimum_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" ] } } }, { "type": "object", "title": "SnmpV3", "description": "SNMPv3 USM AuthPriv — security name + auth/priv protocols and passwords.", "required": [ "security_name", "auth_protocol", "auth_password", "priv_protocol", "priv_password", "type" ], "properties": { "auth_password": { "$ref": "#/components/schemas/SecretValue", "description": "Authentication passphrase." }, "auth_protocol": { "$ref": "#/components/schemas/SnmpV3AuthProtocol", "description": "Hash algorithm used for authentication." }, "context_name": { "type": [ "string", "null" ], "description": "Optional context name (default/empty context used if unset)." }, "priv_password": { "$ref": "#/components/schemas/SecretValue", "description": "Privacy passphrase." }, "priv_protocol": { "$ref": "#/components/schemas/SnmpV3PrivProtocol", "description": "Cipher used for privacy (encryption)." }, "security_name": { "type": "string", "description": "USM security (user) name." }, "type": { "type": "string", "enum": [ "SnmpV3" ] } } }, { "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": [ { "type": "null" }, { "$ref": "#/components/schemas/FileOrInline", "description": "PEM-encoded public certificate — inline or file path on daemon host" } ] }, "ssl_chain": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/FileOrInline", "description": "PEM-encoded CA chain — inline or file path on daemon host" } ] }, "ssl_key": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SecretValue", "description": "Private key — inline PEM content or file path on daemon host" } ] }, "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": { "type": [ "string", "null" ], "description": "Path to the Docker socket. Blank lets the daemon auto-detect it." }, "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": [ { "type": "null" }, { "$ref": "#/components/schemas/FileOrInline", "description": "PEM-encoded public certificate — inline or file path on daemon host" } ] }, "ssl_chain": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/FileOrInline", "description": "PEM-encoded CA chain — inline or file path on daemon host" } ] }, "ssl_key": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/SecretValue", "description": "Private key — inline PEM content or file path on daemon host" } ] }, "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": { "type": [ "string", "null" ], "description": "Path to the Podman socket. Blank lets the daemon auto-detect it." }, "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." } } } ], "description": "Universal credential type — tagged enum stored as JSONB.\nEach variant represents a different credential protocol/method." }, "CredentialTypeDiscriminants": { "type": "string", "enum": [ "SnmpV1", "SnmpV2c", "SnmpV3", "DockerProxy", "DockerSocket", "PodmanProxy", "PodmanSocket", "UnifiApiKey", "UnifiLocalAdmin", "InstantOnAccount" ] }, "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", "network_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 network-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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." } } }, "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", "network_id", "url", "mode", "name", "tags", "user_id" ], "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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network 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.-]+)?$" } } }, "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", "network_id" ] }, "DaemonOs": { "type": "string", "description": "Operating system the install command was generated for.", "enum": [ "linux", "macos", "windows", "freebsd" ] }, "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", "network_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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network 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": [ { "type": "null" }, { "$ref": "#/components/schemas/ServerCapabilities", "description": "Server capabilities (returned if daemon sends version info)" } ] } } }, "DaemonResponse": { "allOf": [ { "$ref": "#/components/schemas/DaemonBase" }, { "type": "object", "required": [ "id", "created_at", "updated_at", "version_status", "interfaced_subnet_ids" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "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" }, "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)" } } }, "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." }, "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": { "has_correct_docker_volume_mount": { "type": "boolean", "description": "Whether a containerized daemon is mounted so it can read the Docker socket." }, "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_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 floor." }, "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": [ "networks", "daemons", "recent_discoveries", "plan_usage" ], "properties": { "daemons": { "type": "array", "items": { "$ref": "#/components/schemas/DaemonResponse" }, "description": "Daemons the caller can see, with their current status." }, "networks": { "type": "array", "items": { "$ref": "#/components/schemas/NetworkSummary" }, "description": "Per-network counts for every network the caller can see." }, "plan_usage": { "$ref": "#/components/schemas/PlanUsage", "description": "Current usage against the organization's plan allowances." }, "recent_discoveries": { "type": "array", "items": { "$ref": "#/components/schemas/Discovery" }, "description": "The most recent discovery runs, newest 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", "network_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", "network_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 kind 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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network 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." } } }, "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", "network_id" ] }, "DependencyType": { "type": "string", "enum": [ "RequestPath", "HubAndSpoke" ] }, "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." } } }, "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, `network_credentials` for a broadcast one (see\n[`Discovery::take_network_scope_credential_ids`])." }, "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", "network_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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "run_type": { "$ref": "#/components/schemas/RunType", "description": "Whether this run was triggered by hand or on a schedule." }, "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." }, "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", "network_id", "discovery_type" ] }, "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" ] }, "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 network." }, "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 a single host: re-check the addresses and\nports already recorded for it, rather than sweeping a subnet.\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_id", "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 host being rescanned is `target_host_id`." }, "ips": { "type": "array", "items": { "type": "string" }, "description": "Addresses to scan on that host." }, "ports": { "type": "array", "items": { "$ref": "#/components/schemas/PortType" }, "description": "Ports already known on that host, re-checked to confirm they are\nstill open. Scanned in addition to the standard discovery set, so a\nrescan also surfaces newly-opened services." }, "settings": { "$ref": "#/components/schemas/RescanSettings" }, "target_host_id": { "type": "string", "format": "uuid", "description": "The host being rescanned." }, "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", "network_id", "phase", "discovery_type", "progress" ], "properties": { "daemon_id": { "type": "string", "format": "uuid", "description": "The daemon this entity refers to." }, "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 kind 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 }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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 }, "scanned": { "oneOf": [ { "type": "null" }, { "$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." } ] }, "session_id": { "type": "string", "format": "uuid", "description": "The discovery run this update 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", "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 } } }, { "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", "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 } } }, { "type": "object", "title": "SnmpWalkEntryCap", "description": "Stopped at our own entry cap. The device is fine and larger than we read.", "required": [ "address", "group", "limit", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkEntryCap" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkUnsupported" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkDesynchronised" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkPartialDiscarded" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkPartialRecorded" ] }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "SnmpWalkBridgeMibAbsent", "description": "Nothing came back for the root of the bridge MIB, which switches commonly do not implement.", "required": [ "address", "group", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkBridgeMibAbsent" ] }, "group": { "$ref": "#/components/schemas/SnmpWalkGroup" } } }, { "type": "object", "title": "SnmpWalkNoAnswer", "description": "Nothing came back at all, and the device stopped answering rather than reporting empty.", "required": [ "address", "group", "code" ], "properties": { "address": { "type": "string", "description": "The device this group was read from." }, "code": { "type": "string", "enum": [ "SnmpWalkNoAnswer" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device that published the count." }, "code": { "type": "string", "enum": [ "ClaimedCountReadCutShort" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device that published the count." }, "code": { "type": "string", "enum": [ "ClaimedCountUnderRead" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device that declared the capability." }, "code": { "type": "string", "enum": [ "ClaimedCapabilityReadCutShort" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device that declared the capability." }, "code": { "type": "string", "enum": [ "ClaimedCapabilityEmpty" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device that reported the neighbours." }, "code": { "type": "string", "enum": [ "LldpLocalPortDropped" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device that reported the neighbours." }, "code": { "type": "string", "enum": [ "LldpLocalPortMisplaced" ] }, "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", "code" ], "properties": { "address": { "type": "string", "description": "The device that answered." }, "code": { "type": "string", "enum": [ "SnmpCollectedNothing" ] } } }, { "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": "CredentialTargetNotScanned", "description": "The credential's address is not on any subnet this scan covers.", "required": [ "address", "integration", "code" ], "properties": { "address": { "type": "string", "description": "The address the credential is bound to." }, "code": { "type": "string", "enum": [ "CredentialTargetNotScanned" ] }, "integration": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants" } } }, { "type": "object", "title": "CredentialTargetNotResponding", "description": "Nothing answered at the credential's address during the scan.", "required": [ "address", "integration", "code" ], "properties": { "address": { "type": "string", "description": "The address the credential is bound to." }, "code": { "type": "string", "enum": [ "CredentialTargetNotResponding" ] }, "integration": { "$ref": "#/components/schemas/CredentialQueryPayloadDiscriminants" } } }, { "type": "object", "title": "CredentialGateClosed", "description": "The port the credential needs was not open, so it was never tried.", "required": [ "address", "integration", "ports", "code" ], "properties": { "address": { "type": "string", "description": "The address the credential is bound to." }, "code": { "type": "string", "enum": [ "CredentialGateClosed" ] }, "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": "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 } } }, { "allOf": [ { "$ref": "#/components/schemas/UnmatchedNeighbour", "description": "The advertised identifier matches no host on this network." }, { "type": "object", "required": [ "code" ], "properties": { "code": { "type": "string", "enum": [ "LldpNeighbourNotFound" ] } } } ], "description": "The advertised identifier matches no host on this network." }, { "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." }, { "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." }, "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." } } }, "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." } } }, "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": { "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": "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." }, "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." } } }, { "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", "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." } } } ] }, "EmailInstallCommandRequest": { "type": "object", "description": "Request body for emailing an install command to the authenticated user.", "required": [ "install_command", "os" ], "properties": { "install_command": { "type": "string", "description": "The install command to send, exactly as shown in the UI." }, "os": { "$ref": "#/components/schemas/DaemonOs", "description": "Operating system the command targets, used to pick the email wording." } } }, "EmailSettings": { "type": "object", "description": "Per-user toggles for the user-pausable email categories. Each field maps\n1:1 to a [`PausableCategory`]; required emails 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." }, "trial_and_usage": { "type": "boolean", "description": "Send trial reminders and plan-usage warnings." } } }, "EnterpriseInquiryRequest": { "type": "object", "description": "Enterprise plan inquiry request", "required": [ "email", "name", "company", "team_size", "message" ], "properties": { "company": { "type": "string", "description": "Company name" }, "email": { "type": "string", "format": "email", "description": "Contact email" }, "message": { "type": "string", "description": "Message/use case description" }, "name": { "type": "string", "description": "Contact name" }, "network_count": { "type": [ "integer", "null" ], "format": "int64", "description": "Number of networks/sites" }, "plan_type": { "type": [ "string", "null" ], "description": "Plan the enquiry is about — the `type` tag of a `BillingPlan`\n(e.g. `Team`, `Business`, `Enterprise`)." }, "team_size": { "$ref": "#/components/schemas/TeamSize", "description": "Team/company size" }, "urgency": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/InquiryTimeline", "description": "How soon they want to move" } ] } } }, "EntityDiscriminants": { "type": "string", "enum": [ "Organization", "Invite", "Share", "Network", "DaemonApiKey", "UserApiKey", "User", "Tag", "Discovery", "Daemon", "Host", "Service", "Port", "Binding", "IPAddress", "Interface", "Credential", "Subnet", "Vlan", "Dependency", "Topology", "Snapshot", "Unknown" ] }, "EntityFreshness": { "type": "string", "description": "How recently discovery last observed an entity.\n\nDerived, never persisted — computed from `last_seen_at` against the\nentity's network staleness window (`Network::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 network 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": "Unknown", "required": [ "type" ], "properties": { "type": { "type": "string", "enum": [ "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." } } }, "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": { "type": "string", "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." }, "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." } } }, "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)." } } }, "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 network 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": { "created_at": "2026-01-15T10:30:00Z", "credential_assignments": [], "description": "Primary web server", "first_discovery_id": null, "hidden": false, "hostname": "web-server-01.local", "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", "network_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 } }, "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`]." }, { "type": "object", "required": [ "network_id", "hostname", "description", "source", "virtualization_metadata", "virtualization_service_id", "hidden", "tags", "credential_assignments" ], "properties": { "chassis_id": { "type": [ "string", "null" ], "description": "LLDP lldpLocChassisId - globally unique device identifier for deduplication" }, "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." }, "hidden": { "type": "boolean", "description": "Whether the host is hidden from topology views." }, "hostname": { "type": [ "string", "null" ], "description": "Hostname as resolved or reported by the host." }, "management_url": { "type": [ "string", "null" ], "format": "uri", "description": "URL for device management interface (manual or discovered)" }, "manufacturer": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalMfgName - hardware manufacturer" }, "model": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalModelName - hardware model" }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "serial_number": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalSerialNum - hardware serial number" }, "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.0 - admin contact info" }, "sys_descr": { "type": [ "string", "null" ], "description": "SNMP sysDescr.0 - full system description" }, "sys_location": { "type": [ "string", "null" ], "description": "SNMP sysLocation.0 - physical location" }, "sys_name": { "type": [ "string", "null" ], "description": "SNMP sysName.0 - administratively-assigned hostname" }, "sys_object_id": { "type": [ "string", "null" ], "description": "SNMP sysObjectID.0 - vendor OID for device identification" }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." }, "virtualization_metadata": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." } ] }, "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." }, "HostName": { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "Human-facing name for the host." }, "name_source": { "$ref": "#/components/schemas/HostNameSource" } } }, "HostNameSource": { "type": "string", "enum": [ "Unnamed", "Unspecified", "Ip", "DetectedService", "Hostname", "DnsSd", "Integration", "Manual" ] }, "HostNamingFallback": { "type": "string", "enum": [ "Ip", "BestService" ] }, "HostOrderField": { "type": "string", "description": "Fields that hosts can be ordered/grouped by.", "enum": [ "created_at", "name", "hostname", "updated_at", "virtualized_by", "network_id", "interface_ip", "last_seen_at" ] }, "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", "network_id", "source", "hidden", "tags", "ip_addresses", "ports", "services", "interfaces" ], "properties": { "chassis_id": { "type": [ "string", "null" ], "description": "LLDP chassis identifier, used to match the host to its neighbours." }, "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." }, "hidden": { "type": "boolean", "description": "Whether the host is hidden from topology views." }, "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", "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_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 rest of the SCD2/audit columns are not." }, "management_url": { "type": [ "string", "null" ], "description": "Link to the host's own management interface." }, "manufacturer": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalMfgName — hardware manufacturer. Read-only, as above.", "readOnly": true }, "model": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalModelName — hardware model. Read-only, as above.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for the host." }, "name_source": { "$ref": "#/components/schemas/HostNameSource", "description": "Which rung of the naming ladder produced `name`. Read-only: it is decided by whoever\nsupplied the name, not by the caller." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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 }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Services running on this host." }, "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_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_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_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." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "virtualization_metadata": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } }, "example": { "created_at": "2026-01-15T10:30:00Z", "credential_assignments": [], "description": "Primary web server", "hidden": false, "hostname": "web-server-01.local", "id": "550e8400-e29b-41d4-a716-446655440003", "interfaces": [ { "admin_status": "Up", "cdp_address": null, "cdp_device_id": null, "cdp_platform": null, "cdp_port_id": null, "created_at": "2026-01-15T10:30:00Z", "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, "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "lldp_chassis_id": null, "lldp_mgmt_addr": null, "lldp_port_desc": null, "lldp_port_id": null, "lldp_sys_desc": null, "lldp_sys_name": null, "mac_address": "DE:AD:BE:EF:CA:FE", "neighbor": null, "neighbor_seen_at": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "oper_status": "Up", "speed_bps": 1000000000, "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "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", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "name": "eth0", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "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_seen_at": "2026-01-15T10:30:00Z", "name": "web-server-01", "name_source": "Manual", "network_id": "550e8400-e29b-41d4-a716-446655440002", "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, "network_id": "550e8400-e29b-41d4-a716-446655440002", "number": 80, "protocol": "Tcp", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "services": [ { "bindings": [ { "created_at": "2026-08-26T01:22:52.422459Z", "first_discovery_id": null, "id": "4b7c9665-d5f2-446b-a910-e830566f5620", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-08-26T01:22:52.422459Z", "lineage_id": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "type": "Port", "updated_at": "2026-08-26T01:22:52.422459Z", "valid_from": "2026-08-26T01:22:52.422459Z", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "service_definition": "Jotty", "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 } ], "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "virtualization_metadata": null, "virtualization_service_id": null } }, "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" ] } } } ], "title": "HostVirtualization" }, "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", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "name": "eth0", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "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": { "type": "object", "required": [ "network_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" }, "mac_address": { "type": [ "string", "null" ], "description": "MAC address discovered from ARP, SNMP, or Docker - immutable once set", "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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "position": { "type": "integer", "format": "int32", "description": "Position of this IP address in the host's IP address list (for ordering)" }, "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." }, "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" ] }, "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", "group_id", "role" ], "properties": { "entity_id": { "type": "string", "format": "uuid", "description": "The inlined entity's ID (e.g., service ID)." }, "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" ] }, "InquiryTimeline": { "type": "string", "description": "How soon the enquirer wants to move.", "enum": [ "immediately", "1-3 months", "3-6 months", "exploring" ] }, "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." } } }, "InstallCommandKind": { "type": "string", "description": "What the caller wants the command to do — the one axis that actually varies.\n\n`install` brings a daemon up (or re-keys a legacy one): it carries the api-key placeholder,\nfetches the binary, and spells out the connectivity + advanced config. `reconfigure` adjusts\nan already-installed daemon in place: no key, no fetch, just the server-held connectivity —\n`scanopy-daemon install` layers it over the existing `config.json`. There is no third case:\nre-asserting the record's (correct) values on an installed daemon is harmless, so a first\ninstall and a re-key are the same command.", "enum": [ "install", "reconfigure" ] }, "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": "Network", "description": "All hosts on the network — a broadcast default credential.", "required": [ "credential_id", "scope" ], "properties": { "credential_id": { "type": "string", "format": "uuid", "description": "Credential to use across the network." }, "scope": { "type": "string", "enum": [ "Network" ] } } }, { "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 }, "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": { "type": "object", "required": [ "host_id", "network_id", "if_index", "if_descr", "if_type", "admin_status", "oper_status" ], "properties": { "admin_status": { "$ref": "#/components/schemas/IfAdminStatus", "description": "SNMP ifAdminStatus: 1=up, 2=down, 3=testing" }, "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" }, "fdb_macs": { "type": [ "array", "null" ], "items": { "type": "string" }, "description": "Bridge FDB: learned MAC addresses on this switch port.\nSingle-MAC ports can be resolved to neighbor links server-side.\nMulti-MAC ports indicate uplinks where LLDP/CDP is the better source." }, "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", "description": "SNMP ifDescr - interface description (e.g., GigabitEthernet0/1)" }, "if_index": { "type": "integer", "format": "int32", "description": "SNMP ifIndex - stable identifier within device" }, "if_name": { "type": [ "string", "null" ], "description": "SNMP ifName - short interface name (e.g., Gi1/0/1)" }, "if_type": { "type": "integer", "format": "int32", "description": "SNMP ifType - IANAifType integer (6=ethernet, 24=loopback, etc.)" }, "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\"." }, "lldp_chassis_id": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/LldpChassisId", "description": "Remote chassis identifier from LLDP neighbor (globally/locally unique)" } ] }, "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": [ { "type": "null" }, { "$ref": "#/components/schemas/LldpPortId", "description": "Remote port identifier from LLDP neighbor" } ] }, "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)" }, "mac_address": { "type": [ "string", "null" ], "description": "MAC address from SNMP ifPhysAddress - immutable once set", "example": "a4:bb:6d:12:34:56", "pattern": "^(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$" }, "native_vlan_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Native/untagged VLAN entity ID on this port (resolved from Q-BRIDGE dot1qPvid)" }, "neighbor": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/Neighbor", "description": "Resolved neighbor connection (mutually exclusive: either Interface or Host)" } ] }, "neighbor_seen_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When a scan last carried evidence that something is adjacent to this port.\n\nThe freshness subject for the *link*, as `last_seen_at` is for the port. A port keeps\nappearing in the ifTable long after its neighbour record stops arriving, so `last_seen_at`\ncannot tell a live adjacency from one whose evidence has vanished. Judged against the same\n`Network::stale_cutoff` as every other freshness verdict.\n\n`None` means no scan has ever carried evidence for this row, and reads as *unknown* —\nnever as stale. Server-owned: stamped on the discovery ingest path, never sent by a daemon.", "readOnly": true }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "oper_status": { "$ref": "#/components/schemas/IfOperStatus", "description": "SNMP ifOperStatus: 1=up, 2=down, 3=testing, 4=unknown, 5=dormant, 6=notPresent, 7=lowerLayerDown" }, "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)" } } }, "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 creating an SNMP interface entry (ifTable data).\nUsed in CreateHostRequest. Server assigns UUIDs since nothing references\nInterface IDs at creation time (neighbor resolution is done server-side).", "required": [ "if_index", "if_descr" ], "properties": { "admin_status": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/IfAdminStatus", "description": "SNMP ifAdminStatus" } ] }, "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": [ { "type": "null" }, { "$ref": "#/components/schemas/IfOperStatus", "description": "SNMP ifOperStatus" } ] }, "speed_bps": { "type": [ "integer", "null" ], "format": "int64", "description": "Interface speed in bits per second" } } }, "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", "network_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." }, "network_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The networks this entity applies to." }, "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" }, "url": { "type": "string", "description": "Link the recipient follows to accept the invite." } } }, "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." } } }, "LicenseStatusDiscriminants": { "type": "string", "description": "Runtime license state as reported by the public config endpoint.", "enum": [ "valid", "expired", "invalid" ] }, "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 } } }, "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" ], "properties": { "address": { "type": "string", "description": "The device that reported the records." }, "consequence": { "$ref": "#/components/schemas/MalformedNeighbourConsequence" }, "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 } } }, "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": {}, "description": "Tuple of [name: string, children: MatchReason[]]" }, "type": { "type": "string", "enum": [ "container" ] } } } ], "description": "Match reason - either a simple reason string or a container with nested reasons" }, "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": "Resolved LLDP/CDP neighbor connection.\n\nRepresents the remote endpoint this port connects to, discovered via LLDP or CDP.\nThe two variants are mutually exclusive and represent different resolution states." }, "Network": { "allOf": [ { "$ref": "#/components/schemas/NetworkBase" }, { "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 Network", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "stale_after_hours": null, "tags": [], "updated_at": "2026-01-15T10:30:00Z" } }, "NetworkBase": { "type": "object", "required": [ "name", "organization_id", "tags", "credential_ids", "stale_after_hours" ], "properties": { "credential_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Credential IDs associated with this network (hydrated from junction table)." }, "name": { "type": "string", "description": "Human-facing name for this network." }, "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 network may go unobserved\nbefore it reads as stale. `None` = unset; callers resolve the effective\nvalue through [`Network::stale_after`], never by reading this directly.\n\nNetwork-scoped because staleness is only meaningful relative to scan\ncadence, and cadence is a property of a network's discoveries." }, "tags": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Tags assigned to this entity." } } }, "NetworkSetup": { "type": "object", "description": "Network configuration for setup", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "Name for the network created during setup." } } }, "NetworkSummary": { "type": "object", "description": "Per-network 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 network.", "minimum": 0 }, "host_count": { "type": "integer", "format": "int64", "description": "Hosts currently discovered on this network.", "minimum": 0 }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "name": { "type": "string", "description": "Name of the network." }, "service_count": { "type": "integer", "format": "int64", "description": "Services currently discovered on this network.", "minimum": 0 }, "subnet_count": { "type": "integer", "format": "int64", "description": "Subnets currently known on this network.", "minimum": 0 } } }, "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": [ { "type": "null" }, { "$ref": "#/components/schemas/Color", "description": "Display color (set by graph builder from the source entity, e.g. subnet type)" } ] }, "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." }, "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." } } }, "OnboardingNetworkState": { "type": "object", "description": "Network data in onboarding state response", "required": [ "name" ], "properties": { "id": { "type": [ "string", "null" ], "format": "uuid", "description": "Network ID (if created)" }, "name": { "type": "string", "description": "Network name" } } }, "OnboardingOperationDiscriminants": { "type": "string", "enum": [ "OrgCreated", "OnboardingModalCompleted", "PlanSelected", "DaemonPromptDismissed", "DaemonPromptAccepted", "FirstDaemonRegistered", "FirstTopologyRebuild", "FirstDiscoveryCompleted", "FirstHostDiscovered", "SecondNetworkCreated", "FirstTagCreated", "FirstDependencyCreated", "FirstUserApiKeyCreated", "FirstSnmpCredentialCreated", "FirstApplicationTagCreated", "FirstCredentialCreated", "FirstSnapshotCreated", "InviteSent", "InviteAccepted", "ProfileCompleted", "ReferralSourceCompleted" ] }, "OnboardingStateResponse": { "type": "object", "description": "Response from onboarding state endpoint", "properties": { "network": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/OnboardingNetworkState", "description": "Network from pending setup (with name and ID)" } ] }, "network_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Network ID from pending setup (if any)" }, "org_name": { "type": [ "string", "null" ], "description": "Organization name from pending setup" }, "step": { "type": [ "string", "null" ], "description": "Current onboarding step (if any)" }, "use_case": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/UseCase", "description": "Use case selection (homelab, company, msp)" } ] } } }, "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": [ { "type": "null" }, { "$ref": "#/components/schemas/UseCase", "description": "Use case selection (homelab, company, msp)" } ] } } }, "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": { "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": [ { "type": "null" }, { "$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\"." } ] }, "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 }, "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" }, "description": "Progress through first-run setup." }, "plan": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/BillingPlan", "description": "The plan this organization is on." } ] }, "plan_status": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/PlanStatus", "description": "Current billing state of that plan." } ] }, "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)" } } }, "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.17.13" } }, "example": { "api_version": 1, "pagination": { "has_more": true, "limit": 50, "offset": 0, "total_count": 142 }, "server_version": "0.17.13" } }, "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" ], "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_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" ], "properties": { "created_at": { "type": "string", "format": "date-time", "description": "When this record was first created." }, "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", "network_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, `network_credentials` for a broadcast one (see\n[`Discovery::take_network_scope_credential_ids`])." }, "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", "network_id", "source", "hidden", "tags", "ip_addresses", "ports", "services", "interfaces" ], "properties": { "chassis_id": { "type": [ "string", "null" ], "description": "LLDP chassis identifier, used to match the host to its neighbours." }, "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." }, "hidden": { "type": "boolean", "description": "Whether the host is hidden from topology views." }, "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", "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_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 rest of the SCD2/audit columns are not." }, "management_url": { "type": [ "string", "null" ], "description": "Link to the host's own management interface." }, "manufacturer": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalMfgName — hardware manufacturer. Read-only, as above.", "readOnly": true }, "model": { "type": [ "string", "null" ], "description": "ENTITY-MIB entPhysicalModelName — hardware model. Read-only, as above.", "readOnly": true }, "name": { "type": "string", "description": "Human-facing name for the host." }, "name_source": { "$ref": "#/components/schemas/HostNameSource", "description": "Which rung of the naming ladder produced `name`. Read-only: it is decided by whoever\nsupplied the name, not by the caller." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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 }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Services running on this host." }, "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_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_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_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." }, "updated_at": { "type": "string", "format": "date-time", "description": "When this record was last modified." }, "virtualization_metadata": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." } ] }, "virtualization_service_id": { "type": [ "string", "null" ], "format": "uuid", "description": "The hypervisor service this VM runs on." } }, "example": { "created_at": "2026-01-15T10:30:00Z", "credential_assignments": [], "description": "Primary web server", "hidden": false, "hostname": "web-server-01.local", "id": "550e8400-e29b-41d4-a716-446655440003", "interfaces": [ { "admin_status": "Up", "cdp_address": null, "cdp_device_id": null, "cdp_platform": null, "cdp_port_id": null, "created_at": "2026-01-15T10:30:00Z", "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, "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "lldp_chassis_id": null, "lldp_mgmt_addr": null, "lldp_port_desc": null, "lldp_port_id": null, "lldp_sys_desc": null, "lldp_sys_name": null, "mac_address": "DE:AD:BE:EF:CA:FE", "neighbor": null, "neighbor_seen_at": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "oper_status": "Up", "speed_bps": 1000000000, "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "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", "last_discovery_id": null, "last_seen_at": "2026-01-15T10:30:00Z", "lineage_id": null, "mac_address": "DE:AD:BE:EF:CA:FE", "name": "eth0", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "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_seen_at": "2026-01-15T10:30:00Z", "name": "web-server-01", "name_source": "Manual", "network_id": "550e8400-e29b-41d4-a716-446655440002", "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, "network_id": "550e8400-e29b-41d4-a716-446655440002", "number": 80, "protocol": "Tcp", "type": "Http", "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null } ], "services": [ { "bindings": [ { "created_at": "2026-08-26T01:22:52.410661Z", "first_discovery_id": null, "id": "d7dcbfc3-a159-4278-adc8-a2ed83296705", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-08-26T01:22:52.410661Z", "lineage_id": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "type": "Port", "updated_at": "2026-08-26T01:22:52.410661Z", "valid_from": "2026-08-26T01:22:52.410661Z", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "service_definition": "Jotty", "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 } ], "source": { "type": "Manual" }, "tags": [], "updated_at": "2026-01-15T10:30:00Z", "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_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-08-26T01:22:52.441246Z", "first_discovery_id": null, "id": "ff5b4999-fda2-4312-b50e-c778574d464a", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-08-26T01:22:52.441246Z", "lineage_id": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "type": "Port", "updated_at": "2026-08-26T01:22:52.441246Z", "valid_from": "2026-08-26T01:22:52.441246Z", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "service_definition": "Jotty", "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_Subnet": { "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/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", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "subnet_type": "Lan", "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": 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_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", "id": "550e8400-e29b-41d4-a716-44665544000a", "is_application": false, "lineage_id": null, "name": "production", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "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." } } }, "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_networks": { "type": [ "integer", "null" ], "format": "int64", "description": "Networks included before per-network 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 }, "network_cents": { "type": [ "integer", "null" ], "format": "int64", "description": "Charge per network beyond `included_networks`, in cents." }, "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." }, "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", "network_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 }, "network_count": { "type": "integer", "format": "int64", "description": "Networks currently counted against the plan.", "minimum": 0 }, "network_limit": { "type": [ "integer", "null" ], "format": "int64", "description": "Networks 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 } } }, "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." } } }, "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, "network_id": "550e8400-e29b-41d4-a716-446655440002", "number": 80, "protocol": "Tcp", "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", "network_id" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "The host this entity belongs to." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." } } } ], "description": "The base data for a Port entity (everything except id, created_at, updated_at)" }, "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)" } } }, "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", "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`/`network_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." }, "network_id": { "type": [ "string", "null" ], "format": "uuid", "description": "Network this daemon will be associated with. Required unless `daemon_id` is set, in\nwhich case the existing record's network is kept." }, "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." }, "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 } } }, "ProxmoxVirtualization": { "type": "object", "properties": { "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", "billing_enabled", "discount_save_offer_available", "has_integrated_daemon", "has_email_service", "has_email_opt_in", "public_url", "needs_cookie_consent", "deployment_type", "license_in_grace_period", "org_limit_reached", "server_admin_contact_email" ], "properties": { "billing_enabled": { "type": "boolean", "description": "Whether this deployment has billing configured." }, "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_opt_in": { "type": "boolean", "description": "Whether the deployment asks users to opt in to product email." }, "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_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_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_status": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/LicenseStatusDiscriminants", "description": "Runtime state of the configured license key. `None` on deployments\nthat don't require one (community and cloud)." } ] }, "needs_cookie_consent": { "type": "boolean", "description": "Whether the client should show a cookie-consent prompt." }, "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." } } }, "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." }, "marketing_opt_in": { "type": "boolean", "description": "Whether the user agreed to receive product and marketing email." }, "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." } } }, "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 }, "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" ] } } } ] }, "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 }, "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." }, "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." } } }, "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": { "type": "string", "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." }, "ServerCapabilities": { "type": "object", "description": "Server capabilities returned on startup/registration", "required": [ "server_version", "minimum_daemon_version" ], "properties": { "deprecation_warnings": { "type": "array", "items": { "$ref": "#/components/schemas/DeprecationWarning" }, "description": "Deprecation warnings for the daemon" }, "minimum_daemon_version": { "type": "string", "description": "Minimum daemon version supported by this server" }, "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-08-26T01:22:52.422976Z", "first_discovery_id": null, "id": "940f1156-d478-408d-8b65-4cd857749848", "ip_address_id": "550e8400-e29b-41d4-a716-446655440005", "last_discovery_id": null, "last_seen_at": "2026-08-26T01:22:52.422976Z", "lineage_id": null, "network_id": "550e8400-e29b-41d4-a716-446655440002", "port_id": "550e8400-e29b-41d4-a716-446655440006", "service_id": "550e8400-e29b-41d4-a716-446655440007", "type": "Port", "updated_at": "2026-08-26T01:22:52.422976Z", "valid_from": "2026-08-26T01:22:52.422976Z", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "position": 0, "service_definition": "Jotty", "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", "network_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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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." }, "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_metadata": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ServiceVirtualization", "description": "Container runtime the service runs in, when it is containerized." } ] }, "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", "Printer", "Database", "Development", "Dashboard", "MessageQueue", "IdentityAndAccess", "Integration", "Office", "ProjectManagement", "Messaging", "Conferencing", "Telephony", "Email", "Publishing", "Unknown", "Custom", "Scanopy", "OpenPorts" ] }, "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": [ { "type": "null" }, { "$ref": "#/components/schemas/ServiceVirtualization", "description": "Container identity (name, id, compose project) if this service is a container." } ] }, "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", "network_id", "position", "service_definition", "last_seen_at" ] }, "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/network configuration", "required": [ "organization_name", "network" ], "properties": { "network": { "$ref": "#/components/schemas/NetworkSetup", "description": "The first network to create alongside the organization." }, "organization_name": { "type": "string", "description": "Name for the organization created during setup." } } }, "SetupResponse": { "type": "object", "description": "Response from setup endpoint", "required": [ "network_id" ], "properties": { "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." } } }, "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", "network_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." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "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." }, "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." } } }, "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": [ "network_id", "taken_at" ], "properties": { "created_by_user_id": { "type": [ "string", "null" ], "format": "uuid", "description": "User who took the snapshot." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "taken_at": { "type": "string", "format": "date-time", "description": "The point in time this snapshot captures." } } }, "SnmpV3AuthProtocol": { "type": "string", "description": "SNMPv3 USM authentication protocol. Variants are limited to the modern,\nsecure set Scanopy supports; MD5 / SHA-2 variants beyond these are\nintentionally excluded. Serialized form (e.g. \"Sha256\") is the wire value\nstored in the credential and used as the frontend select option value.", "enum": [ "Sha1", "Sha256" ] }, "SnmpV3PrivProtocol": { "type": "string", "description": "SNMPv3 USM privacy (encryption) protocol.", "enum": [ "Aes128", "Aes256" ] }, "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" ] }, "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", "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", "network_id": "550e8400-e29b-41d4-a716-446655440002", "source": { "type": "Manual" }, "subnet_type": "Lan", "tags": [], "updated_at": "2026-01-15T10:30:00Z", "valid_from": "2026-01-15T10:30:00Z", "valid_to": null, "virtualization_service_id": null } }, "SubnetBase": { "type": "object", "required": [ "cidr", "network_id", "name", "subnet_type", "virtualization_service_id", "source", "tags" ], "properties": { "cidr": { "type": "string", "description": "Subnet in CIDR notation, IPv4 or IPv6.", "example": "192.168.1.0/24", "pattern": "^[0-9A-Fa-f.:]+/\\d{1,3}$" }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the subnet." }, "name": { "type": "string", "description": "Human-facing name for this subnet." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "source": { "$ref": "#/components/schemas/EntitySource", "description": "Will be automatically set to Manual for creation through API" }, "subnet_type": { "$ref": "#/components/schemas/SubnetType", "description": "What kind of subnet this is — physical, virtual, container bridge, and so on." }, "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 network.\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." } } }, "SubnetOrderField": { "type": "string", "description": "Fields that subnets can be ordered/grouped by.", "enum": [ "created_at", "name", "cidr", "subnet_type", "updated_at", "network_id", "last_seen_at" ] }, "SubnetType": { "type": "string", "enum": [ "Internet", "Remote", "Gateway", "VpnTunnel", "Dmz", "Lan", "WiFi", "IoT", "Guest", "DockerBridge", "PodmanBridge", "MacVlan", "IpVlan", "Management", "Storage", "Loopback", "Unknown" ] }, "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", "id": "550e8400-e29b-41d4-a716-44665544000a", "is_application": false, "lineage_id": null, "name": "production", "organization_id": "550e8400-e29b-41d4-a716-446655440001", "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" ], "properties": { "color": { "$ref": "#/components/schemas/Color", "description": "Colour the tag is drawn in." }, "description": { "type": [ "string", "null" ], "description": "Free-text notes about the tag." }, "is_application": { "type": "boolean", "description": "Whether this tag groups an application, so it drives the application view." }, "name": { "type": "string", "description": "Human-facing name for this tag." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." } } }, "TagOrderField": { "type": "string", "description": "Fields that tags can be ordered/grouped by.", "enum": [ "created_at", "name", "color", "updated_at", "is_application" ] }, "TeamSize": { "type": "string", "description": "Company size bracket offered by the plan-inquiry form.", "enum": [ "1-10", "11-25", "26-50", "51-100", "101-250", "251-500", "501-1000", "1001+" ] }, "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" } } }, "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": [ "network_id", "options" ], "properties": { "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "options": { "$ref": "#/components/schemas/TopologyOptions", "description": "Saved layout and view settings for this topology." } } }, "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 network's grouping options\n(`build_all_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" ], "properties": { "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" ] } }, "hosts": { "type": "array", "items": { "$ref": "#/components/schemas/Host" }, "description": "Hosts included in this topology." }, "interfaces": { "type": "array", "items": { "$ref": "#/components/schemas/Interface" }, "description": "Interfaces included in this topology." }, "ip_addresses": { "type": "array", "items": { "$ref": "#/components/schemas/IPAddress" }, "description": "IP addresses included in this topology." }, "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/Service" }, "description": "Services included in this topology." }, "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." } } }, "TopologyLocalOptions": { "type": "object", "properties": { "bundle_edges": { "type": "boolean", "description": "Collapse parallel edges between the same pair of nodes into one.", "default": true }, "hide_edge_types": { "type": "array", "items": { "$ref": "#/components/schemas/EdgeTypeDiscriminants" }, "description": "Edge types to leave out of the drawing.", "default": [ "Hypervisor" ] }, "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": { "oneOf": [ { "$ref": "#/components/schemas/TopologyTagFilter", "description": "Restrict the view to entities carrying these tags." } ], "default": { "hidden_host_tag_ids": [], "hidden_service_tag_ids": [], "hidden_subnet_tag_ids": [] } } } }, "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": "848c12f9-a3db-408f-bd96-27236c29201d", "rule": { "ByApplication": { "tag_ids": [] } } } ], "L2Physical": [ { "id": "1525c593-cec3-4253-b18c-0e0661e4b091", "rule": "ByHost" } ], "L3Logical": [ { "id": "769fbaa9-9c76-42fc-a0a0-3ea420c81ce7", "rule": "BySubnet" }, { "id": "24a16be7-6f5c-4a7e-96df-207035928681", "rule": "MergeContainerBridges" } ], "Workloads": [ { "id": "1525c593-cec3-4253-b18c-0e0661e4b091", "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.", "default": [ { "id": "4c37a89c-d565-442d-874d-873ed7543276", "rule": "ByTrunkPort" }, { "id": "a0f96d24-344f-4ff8-95b5-34f92adbecf4", "rule": "ByVLAN" }, { "id": "88f1ef81-9928-4551-a35c-34c5cb82461f", "rule": "ByPortOpStatus" }, { "id": "a75c3b4a-3564-4ee2-aec4-d0197d49252f", "rule": { "ByServiceCategory": { "categories": [ "NetworkCore", "NetworkAccess", "RemoteAccess", "Workstation", "Mobile", "Printer", "OpenPorts" ], "is_infra_rule": true, "title": "Infrastructure" } } }, { "id": "1e9e0479-3d72-4b7f-85b1-1ed52a5c75eb", "rule": { "ByTag": { "tag_ids": [], "title": null } } }, { "id": "0c667ada-0709-4f3b-9bc6-5f8909d4134c", "rule": "ByHypervisor" }, { "id": "82b38c66-b0a3-4ad0-b983-1e29adc8fea1", "rule": "ByContainerRuntime" }, { "id": "9fdadb6c-f612-4212-b407-4e2c59073505", "rule": "ByStack" } ] }, "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 kind 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" ] } }, "propertyNames": { "type": "string", "enum": [ "Organization", "Invite", "Share", "Network", "DaemonApiKey", "UserApiKey", "User", "Tag", "Discovery", "Daemon", "Host", "Service", "Port", "Binding", "IPAddress", "Interface", "Credential", "Subnet", "Vlan", "Dependency", "Topology", "Snapshot", "Unknown" ] } }, "propertyNames": { "type": "string", "description": "Which topology view is being rendered", "enum": [ "L2Physical", "L3Logical", "Workloads", "Application" ] } } } }, "TopologyTagFilter": { "type": "object", "description": "Filter settings for hiding entities by tag in topology visualization.", "properties": { "hidden_host_tag_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Host tag IDs to hide (hosts with these tags will fade out)" }, "hidden_service_tag_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Service tag IDs to hide (services with these tags will be hidden from nodes)" }, "hidden_subnet_tag_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Subnet tag IDs to hide (subnets with these tags will fade out)" } } }, "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." }, "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" ], "properties": { "host_id": { "type": "string", "format": "uuid", "description": "The local device that saw the neighbour, not the far end — the far end is what could not\nbe identified." }, "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", "format": "uuid", "description": "The local device that saw the neighbour, and the port it saw it on." }, "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", "format": "uuid", "description": "The far-end device, already resolved — this is what makes it distinct from\n[`UnmatchedNeighbour`]." } } }, "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", "hidden", "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." }, "hidden": { "type": "boolean", "description": "Hide the host from topology views without deleting it." }, "hostname": { "type": [ "string", "null" ], "description": "Hostname as resolved or reported by the host." }, "id": { "type": "string", "format": "uuid", "description": "Server-assigned unique identifier." }, "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_metadata": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/HostVirtualization", "description": "How the host is virtualized, when it is a VM or container guest." } ] }, "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 } } }, "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." }, "network_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Network IDs this key has access to (hydrated from junction table)" }, "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." }, "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." } } }, "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", "network_ids" ], "properties": { "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 }, "network_ids": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "The networks this entity applies to." }, "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." }, "terms_accepted_at": { "type": [ "string", "null" ], "format": "date-time", "description": "When the user accepted the terms of service.", "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" } } }, "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": { "type": "object", "required": [ "vlan_number", "name", "network_id", "organization_id" ], "properties": { "description": { "type": [ "string", "null" ], "description": "Free-text notes about the VLAN." }, "name": { "type": "string", "description": "Human-facing name for this VLAN." }, "network_id": { "type": "string", "format": "uuid", "description": "The network this entity belongs to." }, "organization_id": { "type": "string", "format": "uuid", "description": "The organization that owns this record." }, "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`." }, "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." }, "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": [ "network_id", "vlans" ], "properties": { "network_id": { "type": "string", "format": "uuid", "description": "The network 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 } } }, "VlanOrderField": { "type": "string", "enum": [ "created_at", "name", "vlan_number", "updated_at" ] } }, "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": "Networks", "description": "Network 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 network's topology and entities. Created manually via the topology tab; loadable from the snapshots dropdown." }, { "name": "Credentials", "description": "Credentials for network device discovery and management. Supports SNMP, Docker proxy, and other credential types." }, { "name": "Subnets", "description": "IP subnets within networks. Define address ranges and organize hosts by subnet." }, { "name": "Tags", "description": "Custom tags for categorization. Apply labels to entities for filtering and organization." }, { "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": "system", "description": "System information endpoints. Version and compatibility checking." } ] }