{
  "openapi": "3.0.3",
  "info": {
    "title": "Monaveo Public API",
    "version": "2.0.0",
    "description": "REST API for integrating your organisation's Monaveo data with your own systems — read, create, update, and delete across devices, customers, tickets, alerts, threats, and the knowledge base.\n\n## Authentication\nGenerate an API key in the dashboard under **Settings → API Keys** (organisation admin only), then send it as a Bearer token:\n\n```\nAuthorization: Bearer mvk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nKeys are shown exactly once at creation — only a salted hash and a short prefix are stored, so a database read can never recover a usable key. The legacy `X-API-Key` header is still accepted for backwards compatibility, but `Authorization: Bearer` is the recommended scheme.\n\n## Scoped-key permission model\nEach key carries per-resource access levels — **read**, **write** (create/update), or **delete** — where a higher level implies every lower one (delete ⊇ write ⊇ read). When you generate a key you choose either:\n\n- **Full** — read + write + delete on every resource (invoices remain read-only regardless), or\n- **Custom** — pick a level per resource (e.g. `tickets: write`, `devices: read`, `customers: none`).\n\nA request that exceeds the key's granted level is rejected with **403**. Invoices are **read-only by design** — no key, not even a Full key, can create, modify, or delete an invoice (a deliberate money/safety gate).\n\n## Key management\n- **Per-key IP lock** — optionally restrict a key to a comma-separated allowlist of IPs, CIDRs, or ranges. Requests from any other address are rejected with 403.\n- **Rotate** — mint a new secret for an existing key, keeping its name, scopes, IP-lock, and expiry; the old secret stops working immediately.\n- **Revoke** — disable a leaked key instantly. Keys may also carry an optional expiry.\n- **Limit** — up to **10 active keys** per organisation.\n- **Paid plans only** — key creation requires an active paid subscription (a trial or lapsed org receives **402** when generating a key).\n\n## Scoping & limits\nEvery request is strictly scoped to the organisation that owns the key — you can never see or touch another tenant's data. Sensitive columns (remote-access passwords, certificates, tokens, Stripe/myDATA identifiers) are never returned.\n\n**Rate limit:** 120 requests / minute / key.\n\nAll examples below use fictional placeholder data. See also the [developer overview](https://www.monaveo.com/developers) and the [Vulnerability Disclosure Programme](https://www.monaveo.com/security).",
    "contact": {
      "name": "Monaveo Support",
      "email": "support@monaveo.com",
      "url": "https://www.monaveo.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://app.monaveo.com/api/v1/public",
      "description": "EU region"
    },
    {
      "url": "https://us-app.monaveo.com/api/v1/public",
      "description": "US region"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    { "name": "Organisation" },
    { "name": "Devices" },
    { "name": "Customers" },
    { "name": "Tickets" },
    { "name": "Alerts" },
    { "name": "Threats" },
    { "name": "Knowledge Base" },
    { "name": "Invoices" }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": ["Organisation"],
        "summary": "Your organisation",
        "description": "Returns the profile of the organisation that owns the API key.",
        "responses": {
          "200": {
            "description": "Organisation profile",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "client_id": 1001,
                    "name": "Example MSP Ltd",
                    "plan": "advanced",
                    "currency": "GBP",
                    "subscription_status": "active",
                    "country": "GB",
                    "seats": 3
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/devices": {
      "get": {
        "tags": ["Devices"],
        "summary": "List devices",
        "description": "All managed devices/assets for your organisation (max 1000 per page). Requires the `devices` resource at **read** level or higher.",
        "parameters": [
          { "name": "search", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Case-insensitive match on hostname or friendly name" },
          { "name": "os_type", "in": "query", "required": false, "schema": { "type": "string", "example": "windows" }, "description": "Filter by OS type (e.g. windows, linux, darwin)" },
          { "name": "customer_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter by end-customer id" },
          { "name": "active", "in": "query", "required": false, "schema": { "type": "string", "enum": ["true", "false"] }, "description": "Filter by active/inactive devices" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" }
        ],
        "responses": {
          "200": {
            "description": "Device list",
            "content": {
              "application/json": {
                "example": {
                  "count": 2,
                  "data": [
                    {
                      "id": 1,
                      "hostname": "WS-EXAMPLE-01",
                      "friendly_name": "Reception PC",
                      "os_type": "windows",
                      "agent_version": "1.4.36",
                      "public_ip": "203.0.113.10",
                      "vendor_name": "ExampleCorp",
                      "model_name": "Model X",
                      "serial_number": "SN-EXAMPLE-0001",
                      "customer_id": 10,
                      "site_id": 1,
                      "device_uuid": "00000000-0000-0000-0000-000000000001",
                      "is_active": true,
                      "asset_status": "in_use",
                      "billing_category": "workstation",
                      "last_seen": "2026-01-01T12:00:00Z",
                      "created_at": "2025-12-01T09:00:00Z"
                    },
                    {
                      "id": 2,
                      "hostname": "SRV-EXAMPLE-02",
                      "os_type": "linux",
                      "agent_version": "1.4.36",
                      "customer_id": 10
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/devices/{id}": {
      "parameters": [ { "$ref": "#/components/parameters/PathId" } ],
      "get": {
        "tags": ["Devices"],
        "summary": "Get a device",
        "description": "A single device including extended hardware detail. Requires `devices` at **read** level.",
        "responses": {
          "200": {
            "description": "Device",
            "content": {
              "application/json": {
                "example": {
                  "count": 1,
                  "data": [
                    {
                      "id": 1,
                      "hostname": "WS-EXAMPLE-01",
                      "friendly_name": "Reception PC",
                      "os_type": "windows",
                      "agent_version": "1.4.36",
                      "cpu_model": "Example CPU",
                      "ram_total": "16 GB",
                      "serial_number": "SN-EXAMPLE-0001",
                      "customer_id": 10,
                      "site_id": 1,
                      "is_active": true,
                      "asset_status": "in_use",
                      "billing_category": "workstation",
                      "last_seen": "2026-01-01T12:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "tags": ["Devices"],
        "summary": "Update a device",
        "description": "Update editable asset fields on an agent-enrolled device (devices cannot be created via the API — they enrol through the agent). Only the fields you send are changed. Requires `devices` at **write** level.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "friendly_name": { "type": "string", "maxLength": 120 },
                  "asset_status": { "type": "string", "enum": ["in_use", "in_stock", "damaged", "loaned"] },
                  "billing_category": { "type": "string", "enum": ["workstation", "server", "peripheral", "network", "mobile", "hypervisor", "non_billable"] },
                  "customer_id": { "type": "integer", "description": "Reassign to a customer in your org; send 0 to unassign (also clears site_id)" },
                  "site_id": { "type": "integer", "description": "Assign to a customer site in your org; send 0 to clear" },
                  "notes": { "type": "string" }
                }
              },
              "example": { "friendly_name": "Reception PC", "asset_status": "in_use", "customer_id": 10 }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "example": { "ok": true, "updated": ["friendly_name", "asset_status"] } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Devices"],
        "summary": "Delete a device",
        "description": "Permanently delete a device and its child records (metrics, tasks, proxy sessions). Requires `devices` at **delete** level.",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/customers": {
      "get": {
        "tags": ["Customers"],
        "summary": "List customers",
        "description": "Your end customers (max 1000 per page). Requires `customers` at **read** level.",
        "parameters": [
          { "name": "search", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Case-insensitive match on name, email, or domains" },
          { "name": "country", "in": "query", "required": false, "schema": { "type": "string", "example": "GB" }, "description": "Filter by ISO country code" },
          { "name": "active", "in": "query", "required": false, "schema": { "type": "string", "enum": ["true", "false"] }, "description": "Filter by active/inactive customers" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" }
        ],
        "responses": {
          "200": {
            "description": "Customer list",
            "content": {
              "application/json": {
                "example": {
                  "count": 1,
                  "data": [
                    {
                      "id": 10,
                      "name": "Example Customer Ltd",
                      "email": "it@example.com",
                      "phone": "+44 20 0000 0000",
                      "city": "London",
                      "country": "GB",
                      "industry": "Example",
                      "sla_tier": "gold",
                      "is_active": true,
                      "created_at": "2025-11-01T10:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Customers"],
        "summary": "Create a customer",
        "description": "Create a new end customer. Requires `customers` at **write** level. Subject to your org's customer limit, duplicate-domain checks, and EU sanctions/embargo screening on the country.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string" },
                  "domains": { "type": "string", "description": "Comma-separated domain list (max 64); must not collide with an existing customer" },
                  "email": { "type": "string" },
                  "phone": { "type": "string" },
                  "industry": { "type": "string" },
                  "city": { "type": "string" },
                  "address": { "type": "string" },
                  "postal_code": { "type": "string" },
                  "country": { "type": "string", "example": "GB" }
                }
              },
              "example": { "name": "Example Customer Ltd", "domains": "example.com", "email": "it@example.com", "country": "GB" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": { "application/json": { "example": { "id": 42, "status": "created" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "Insufficient key scope, key not permitted from this IP, customer limit reached, or the country is under EU sanctions/embargo",
            "content": { "application/json": { "example": { "error": "This country is subject to EU sanctions/embargo and cannot be onboarded.", "code": "COUNTRY_RESTRICTED" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/customers/{id}": {
      "parameters": [ { "$ref": "#/components/parameters/PathId" } ],
      "patch": {
        "tags": ["Customers"],
        "summary": "Update a customer",
        "description": "Update an existing customer. Only the fields you send are changed. Requires `customers` at **write** level.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "domains": { "type": "string", "description": "Comma-separated (max 64); duplicate-checked against other customers" },
                  "email": { "type": "string" },
                  "phone": { "type": "string" },
                  "address": { "type": "string" },
                  "city": { "type": "string" },
                  "country": { "type": "string", "description": "Re-screened against EU sanctions/embargo" },
                  "postal_code": { "type": "string" },
                  "website": { "type": "string" },
                  "tax_id": { "type": "string" },
                  "industry": { "type": "string" },
                  "notes": { "type": "string" },
                  "is_active": { "type": "boolean" },
                  "sla_tier": { "type": "string" },
                  "timezone": { "type": "string" }
                }
              },
              "example": { "phone": "+44 20 0000 0001", "is_active": true }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "example": { "ok": true, "updated": ["phone", "is_active"] } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Customers"],
        "summary": "Delete a customer",
        "description": "Delete a customer and unassign its devices. Fails with 409 if the customer still has tickets or invoices. Requires `customers` at **delete** level.",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "Customer still has tickets or invoices",
            "content": { "application/json": { "example": { "error": "Cannot delete: this customer still has tickets or invoices. Remove or reassign them first." } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/tickets": {
      "get": {
        "tags": ["Tickets"],
        "summary": "List tickets",
        "description": "Tickets for your organisation (max 500 per page, newest first). Requires `tickets` at **read** level.",
        "parameters": [
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": ["open", "in_progress", "resolved", "closed"] }, "description": "Filter by status" },
          { "name": "priority", "in": "query", "required": false, "schema": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, "description": "Filter by priority" },
          { "name": "customer_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter by customer id" },
          { "name": "search", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Case-insensitive match on subject" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" }
        ],
        "responses": {
          "200": {
            "description": "Ticket list",
            "content": {
              "application/json": {
                "example": {
                  "count": 1,
                  "data": [
                    {
                      "id": 1,
                      "ticket_number": "EX-1001",
                      "subject": "Example ticket subject",
                      "status": "open",
                      "priority": "high",
                      "ticket_type": "issue",
                      "customer_id": 10,
                      "device_id": 1,
                      "assigned_to": 5,
                      "csat_rating": null,
                      "created_at": "2026-01-01T08:00:00Z",
                      "updated_at": "2026-01-01T09:00:00Z",
                      "closed_at": null
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Tickets"],
        "summary": "Create a ticket",
        "description": "Create a ticket. The ticket number is assigned automatically. Requires `tickets` at **write** level. Set `notify: false` for bulk PSA imports to suppress the new-ticket notification email.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["subject"],
                "properties": {
                  "subject": { "type": "string", "maxLength": 500 },
                  "description": { "type": "string" },
                  "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"], "default": "medium" },
                  "ticket_type": { "type": "string", "default": "issue", "maxLength": 40 },
                  "status": { "type": "string", "enum": ["open", "in_progress", "resolved", "closed"], "default": "open" },
                  "customer_id": { "type": "integer", "description": "Must belong to your organisation" },
                  "device_id": { "type": "integer", "description": "Must belong to your organisation" },
                  "contact_email": { "type": "string" },
                  "notify": { "type": "boolean", "default": true, "description": "Send the new-ticket notification email; set false to suppress (bulk imports)" }
                }
              },
              "example": { "subject": "Printer offline in reception", "priority": "high", "customer_id": 10, "device_id": 1, "notify": false }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": { "application/json": { "example": { "id": 5001, "status": "created" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/tickets/{id}": {
      "parameters": [ { "$ref": "#/components/parameters/PathId" } ],
      "patch": {
        "tags": ["Tickets"],
        "summary": "Update a ticket",
        "description": "Update a ticket. Only the fields you send are changed; SLA deadlines are recomputed when priority/customer change and status transitions trigger notifications. Requires `tickets` at **write** level.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subject": { "type": "string", "maxLength": 500 },
                  "description": { "type": "string" },
                  "status": { "type": "string", "enum": ["open", "in_progress", "resolved", "closed"] },
                  "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] },
                  "ticket_type": { "type": "string", "maxLength": 40 },
                  "assigned_to": { "type": "integer", "description": "User in your org; send 0 to unassign" },
                  "customer_id": { "type": "integer", "description": "Customer in your org; send 0 to clear (also clears SLA)" },
                  "device_id": { "type": "integer", "description": "Device in your org; send 0 to clear" }
                }
              },
              "example": { "status": "resolved", "assigned_to": 5 }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "example": { "ok": true, "updated": ["status", "assigned_to"] } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Tickets"],
        "summary": "Delete a ticket",
        "description": "Delete a ticket and its comments/attachments. Fails with 409 if any attachment is under legal hold. Requires `tickets` at **delete** level.",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "An attachment is under legal hold",
            "content": { "application/json": { "example": { "error": "This ticket has attachments under legal hold and cannot be deleted." } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/alerts": {
      "get": {
        "tags": ["Alerts"],
        "summary": "List device alerts",
        "description": "Device alerts for your organisation (max 500 per page, newest first). Requires `alerts` at **read** level.",
        "parameters": [
          { "name": "active", "in": "query", "required": false, "schema": { "type": "string", "enum": ["true"] }, "description": "Set to true to return only unresolved alerts" },
          { "name": "severity", "in": "query", "required": false, "schema": { "type": "string", "example": "warning" }, "description": "Filter by severity" },
          { "name": "device_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter by device id" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" }
        ],
        "responses": {
          "200": {
            "description": "Alert list",
            "content": {
              "application/json": {
                "example": {
                  "count": 1,
                  "data": [
                    {
                      "id": 1,
                      "device_id": 1,
                      "hostname": "WS-EXAMPLE-01",
                      "alert_type": "disk_full:C:",
                      "severity": "warning",
                      "message": "Disk C: 92% · Largest folders: Windows, Users",
                      "value": 92,
                      "threshold": 90,
                      "is_resolved": false,
                      "created_at": "2026-01-01T11:00:00Z",
                      "resolved_at": null,
                      "acknowledged_at": null
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/alerts/{id}": {
      "parameters": [ { "$ref": "#/components/parameters/PathId" } ],
      "patch": {
        "tags": ["Alerts"],
        "summary": "Act on an alert",
        "description": "Resolve, acknowledge, or unacknowledge a system-generated alert. Requires `alerts` at **write** level.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["action"],
                "properties": {
                  "action": { "type": "string", "enum": ["resolve", "acknowledge", "unacknowledge"] }
                }
              },
              "example": { "action": "acknowledge" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Alerts"],
        "summary": "Clear an alert",
        "description": "Delete a **resolved** alert. Returns 409 if the alert is still open — resolve it first. Requires `alerts` at **delete** level.",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "Alert is not resolved yet",
            "content": { "application/json": { "example": { "error": "resolve the alert before deleting it" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/threats": {
      "get": {
        "tags": ["Threats"],
        "summary": "List detected threats",
        "description": "Security detections (Recent Threats) for your organisation (max 500 per page, newest first). Requires `threats` at **read** level.",
        "parameters": [
          { "name": "active", "in": "query", "required": false, "schema": { "type": "string", "enum": ["true"] }, "description": "Set to true to return only unresolved threats" },
          { "name": "severity", "in": "query", "required": false, "schema": { "type": "string", "example": "high" }, "description": "Filter by severity" },
          { "name": "device_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter by device id" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" }
        ],
        "responses": {
          "200": {
            "description": "Threat list",
            "content": {
              "application/json": {
                "example": {
                  "count": 1,
                  "data": [
                    {
                      "id": 1,
                      "device_id": 1,
                      "hostname": "WS-EXAMPLE-01",
                      "customer_name": "Example Customer Ltd",
                      "threat_name": "EICAR-Test-File",
                      "severity": "high",
                      "action_taken": "quarantined",
                      "status": "active",
                      "source": "defender",
                      "detected_at": "2026-01-01T10:30:00Z",
                      "resolved_at": null,
                      "created_at": "2026-01-01T10:30:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/threats/{id}": {
      "parameters": [ { "$ref": "#/components/parameters/PathId" } ],
      "patch": {
        "tags": ["Threats"],
        "summary": "Resolve a threat",
        "description": "Mark a threat detection as resolved. `resolve` is the only supported action. Requires `threats` at **write** level.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": { "type": "string", "enum": ["resolve"] }
                }
              },
              "example": { "action": "resolve" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Threats"],
        "summary": "Delete a threat",
        "description": "Permanently delete a threat detection record. Requires `threats` at **delete** level.",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/kb": {
      "get": {
        "tags": ["Knowledge Base"],
        "summary": "List knowledge-base articles",
        "description": "Article metadata for your organisation (the article body is not included in the list). Requires `kb` at **read** level.",
        "parameters": [
          { "name": "search", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Case-insensitive match on title" },
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": ["draft", "published"] }, "description": "Filter by status" },
          { "name": "visibility", "in": "query", "required": false, "schema": { "type": "string", "enum": ["internal", "public", "portal"] }, "description": "Filter by visibility" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" }
        ],
        "responses": {
          "200": {
            "description": "Article list",
            "content": {
              "application/json": {
                "example": {
                  "count": 1,
                  "data": [
                    {
                      "id": 1,
                      "kb_number": "KB-1001",
                      "category_id": 3,
                      "title": "How to reset the example VPN client",
                      "tags": ["vpn", "example"],
                      "visibility": "internal",
                      "status": "published",
                      "pinned": false,
                      "view_count": 12,
                      "created_at": "2025-12-10T09:00:00Z",
                      "updated_at": "2026-01-01T09:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Knowledge Base"],
        "summary": "Create an article",
        "description": "Create a knowledge-base article. The KB number is assigned automatically. Requires `kb` at **write** level.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title"],
                "properties": {
                  "title": { "type": "string", "maxLength": 500 },
                  "body": { "type": "string" },
                  "tags": { "type": "array", "items": { "type": "string" } },
                  "visibility": { "type": "string", "enum": ["internal", "public", "portal"], "default": "internal" },
                  "status": { "type": "string", "enum": ["draft", "published"], "default": "draft" },
                  "pinned": { "type": "boolean", "default": false },
                  "category_id": { "type": "integer", "description": "Must belong to your organisation" }
                }
              },
              "example": { "title": "How to reset the example VPN client", "body": "Step 1...", "visibility": "internal", "status": "draft", "tags": ["vpn", "example"] }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": { "application/json": { "example": { "id": 88, "kb_number": "KB-1002", "status": "created" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/kb/{id}": {
      "parameters": [ { "$ref": "#/components/parameters/PathId" } ],
      "patch": {
        "tags": ["Knowledge Base"],
        "summary": "Update an article",
        "description": "Update an article. A version snapshot of the previous content is retained. Only the fields you send are changed. Requires `kb` at **write** level.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": { "type": "string", "maxLength": 500 },
                  "body": { "type": "string" },
                  "tags": { "type": "array", "items": { "type": "string" } },
                  "visibility": { "type": "string", "enum": ["internal", "public", "portal"] },
                  "status": { "type": "string", "enum": ["draft", "published"] },
                  "pinned": { "type": "boolean" },
                  "category_id": { "type": "integer", "description": "Category in your org; send 0 to clear" }
                }
              },
              "example": { "status": "published", "pinned": true }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Knowledge Base"],
        "summary": "Delete an article",
        "description": "Permanently delete a knowledge-base article. Requires `kb` at **delete** level.",
        "responses": {
          "200": { "$ref": "#/components/responses/Ok" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/invoices": {
      "get": {
        "tags": ["Invoices"],
        "summary": "List invoices",
        "description": "Invoice headers (financial summary + status) for your organisation (max 500 per page, newest first). Requires `invoices` at **read** level.\n\n**Read-only by design:** invoices cannot be created, updated, or deleted through the public API — this is a deliberate money/safety gate. Even a Full key cannot mutate invoices, and Stripe/myDATA identifiers, payment links, and free-text notes/terms are never returned.",
        "parameters": [
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "example": "paid" }, "description": "Filter by invoice status" },
          { "name": "customer_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Filter by customer id" },
          { "$ref": "#/components/parameters/Limit" },
          { "$ref": "#/components/parameters/Offset" }
        ],
        "responses": {
          "200": {
            "description": "Invoice list",
            "content": {
              "application/json": {
                "example": {
                  "count": 1,
                  "data": [
                    {
                      "id": 1,
                      "invoice_number": "INV-EX-0001",
                      "doc_type": "invoice",
                      "status": "paid",
                      "customer_id": 10,
                      "issue_date": "2026-01-01",
                      "due_date": "2026-01-31",
                      "paid_date": "2026-01-15",
                      "currency": "GBP",
                      "subtotal": 100.0,
                      "tax_amount": 20.0,
                      "total": 120.0,
                      "created_at": "2026-01-01T09:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "mvk_live_…",
        "description": "Send your API key as a Bearer token: `Authorization: Bearer mvk_live_…`. The key's per-resource scope (read / write / delete) determines which operations it may perform; a per-key IP lock may further restrict where it can be used."
      }
    },
    "parameters": {
      "PathId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "integer" },
        "description": "Numeric resource id"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": { "type": "integer", "maximum": 1000 },
        "description": "Maximum rows to return (clamped to the endpoint's cap)"
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "schema": { "type": "integer", "minimum": 0 },
        "description": "Row offset for pagination"
      }
    },
    "responses": {
      "Ok": {
        "description": "Success",
        "content": { "application/json": { "example": { "ok": true } } }
      },
      "BadRequest": {
        "description": "Invalid request (bad id, invalid JSON body, invalid field/enum value, or no updatable fields)",
        "content": { "application/json": { "example": { "error": "invalid JSON body" } } }
      },
      "Unauthorized": {
        "description": "Missing, invalid, revoked, expired, or inactive API key",
        "content": { "application/json": { "example": { "error": "Invalid, revoked, or inactive API key" } } }
      },
      "Forbidden": {
        "description": "The key's scope does not permit this operation, or the key is locked to a different IP address",
        "content": { "application/json": { "example": { "error": "This API key is not permitted to write tickets" } } }
      },
      "PaymentRequired": {
        "description": "API keys are a paid-plan feature — a trial or lapsed organisation cannot generate keys (returned by the dashboard key-creation endpoint, not by the public API itself)",
        "content": { "application/json": { "example": { "error": "API keys are available on paid plans. Subscribe to generate an API key.", "code": "upgrade_required" } } }
      },
      "NotFound": {
        "description": "Resource not found in your organisation",
        "content": { "application/json": { "example": { "error": "not found" } } }
      },
      "Conflict": {
        "description": "The request conflicts with the resource's current state (e.g. legal hold, dependent records, key limit reached)",
        "content": { "application/json": { "example": { "error": "conflict" } } }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (120 requests / minute / key)",
        "content": { "application/json": { "example": { "error": "Rate limit exceeded (120 req/min)" } } }
      }
    }
  }
}
