Skip to main content

Integration API documentation

Integration API Documentation

Table of Contents


Overview

PatchMonPatchMon's Integration API Credentials provideprovides programmatic access to your PatchMon instance, enabling automation, integration with third-party tools, and custom workflows. API credentials use HTTP Basic Authentication with scoped permissions to control access to specific resources and actions.

Key Features

  • Scoped Permissions: Fine-grained control over what each credential can access
  • IP Restrictions: Optional IP allowlisting for enhanced security
  • Expiration Dates: Set automatic expiration for temporary access
  • Basic Authentication: Industry-standard authentication method (RFC 7617)
  • Rate Limiting: Built-in protection against abuse
  • Audit Trail: Track credential usage with last-used timestamps

Use Cases

  • Automation: Integrate PatchMon data into CI/CD pipelines
  • Inventory Management: Use with Ansible, Terraform, or other IaC tools
  • Monitoring: Feed PatchMon data into monitoring dashboards
  • Custom Scripts: Build custom tools that interact with PatchMon
  • Third-Party Integrations: Connect PatchMon to other systems

Interactive API Reference (Swagger)

PatchMon includes a built-in interactive API reference powered by Swagger UI. You can explore all available endpoints, view request/response schemas, and test API calls directly from your browser.

To access the Swagger UI:

https://<your-patchmon-url>/api/v1/api-docs

Note: The Swagger UI requires you to be logged in to PatchMon (JWT authentication). Log in to your PatchMon dashboard first, then navigate to the URL above in the same browser session.

The Swagger reference covers all internal and scoped API endpoints. This documentation page focuses specifically on the scoped Integration API that uses Basic Authentication with API credentials.


Creating API Credentials

Step-by-Step Guide

1. Navigate to Integrations PageSettings

  1. Log in to your PatchMon instance as an administrator
  2. Go to SettingsIntegrations
  3. ClickYou onwill see the Auto-Enrollment & API tab

2. Click "New Credential"Token"

Click the "New Credential"Token" buttonbutton. inA modal will appear where you can select the top-rightcredential corner of the API tab.type.

3. Select "API" as the Usage Type

In the creation modal, select "API" as the usage type. This configures the credential for programmatic access via Basic Authentication.

4. Configure the Credential

Fill in the following fields in the creation form:fields:

Required Fields

Fields:

*

*

  • Select the
mustselected
  • Available
  • scopes
    FieldDescriptionExample
    Token Name A descriptive name for this credential
  • Examples: "Ansible Inventory", "Monitoring Dashboard", "CI/CD Pipeline"
  • Used for identification and audit purposes
  • Ansible

    Inventory, Monitoring Dashboard

    Scopes The permissions this credential should have
  • At(at least one permissionrequired)
  • host: beget
    are detailed in the Available Scopes & Permissions section

    Optional Fields

    Fields:

  • Leave
  • emptyto allow access from any IP address
  • Supports single IP addresses or CIDR notation
    • Set an automatic
    don'texpire
  • Expired
  • credentialsrejected
    1. Review
    2. yourconfiguration
    3. Click "Create Token"
    4. The credential will be generated immediately
    5. FieldDescriptionExample
      Allowed IP Addresses

      Comma-separated list of IPIPs addressesor CIDR ranges that can use this credentialcredential.
    6. Examples:Leave empty for unrestricted access.
    7. 192.168.1.100, 10.0.0.50,100, 10.0.0.510/24
      Expiration Date

      Automatic expiration date for the credentialcredential.
    8. Uses ISO 8601 datetime format (datetime-local input)
    9. Leave empty for credentialsno thatexpiration.
    10. 2026-12-31T23:59:59
      Default areHost automaticallyGroup Optionally assign

      4.a Reviewdefault andhost Create

      group
      Production

      5. Save Your Credentials

      ⚠️ CRITICAL: Save these credentials nowimmediately - the secret cannot be retrieved later!

      After creation, you'lla see asuccess modal displaying:displays:

      • Token Name: Your chosen name
      • Token Key: The API key (username)used as the username in Basic Auth), prefixed with patchmon_ae_
      • Token Secret: The API secret (used as the password) - shown only shown once
      • Granted Scopes: The permissions assigned
      • Usage Examples: Pre-filled cURL commands ready to copy

      Important Actions:

      1. Copy both the Token Key and Token Secret
      2. Storeand store them securely (passwordbefore manager, secrets vault, etc.)
      3. Testclosing the credential immediately
      4. Close the modal only after saving both values

      Configuration Options

      Token Name

      • Required: Yes
      • Type: String
      • Description: Human-readable identifier for the credential
      • Best Practice: Use descriptive names that indicate the credential's purpose
      • Examples:
        • "Production Ansible Inventory"
        • "Monitoring Dashboard - Grafana"
        • "CI/CD Pipeline - Jenkins"

      Scopes

      • Required: Yes
      • Type: Object with arrays
      • Description: Defines what actions the credential can perform on which resources
      • Structure: { "resource": ["action1", "action2"] }
      • Validation: At least one action must be selected for at least one resource

      Allowed IP Ranges

      • Required: No
      • Type: Array of strings (comma-separated in UI)
      • Description: Restricts credential usage to specific IP addresses
      • Format: Single IPs or CIDR notation
      • Examples:
        • 192.168.1.100 - Single IP
        • 10.0.0.0/24 - CIDR range
        • 192.168.1.100, 10.0.0.50 - Multiple IPs
      • Default: Empty (no restrictions)

      Expiration Date

      • Required: No
      • Type: ISO 8601 datetime
      • Description: Automatic expiration date for the credential
      • Format: YYYY-MM-DDTHH:mm:ss
      • Example: 2026-12-31T23:59:59
      • Default: null (no expiration)
      modal.


      Authentication

      Basic Authentication

      PatchMon API credentials use HTTP Basic Authentication as defined in RFC 7617.

      Format

      Authorization: Basic <base64(token_key:token_secret)>
      

      How It Works

      1. Combine your token key and secret with a colon: token_key:token_secret
      2. Encode the combined string in Base64
      3. Prepend "Basic " to the encoded string
      4. Send it in the Authorization header

      Most HTTP clients handle this automatically with thefor example, cURL's -u flag (cURL) or Python's authHTTPBasicAuth parameter (Python requests).

      Authentication Flow

      ┌─────────────┐                                  ┌─────────────┐
      │   Client     │                                  │  PatchMon   │
      │ Application  │                                  │   Server    │
      └──────┬──────┘                                  └──────┬──────┘
             │                                                │
             │  1. Send Requestrequest with Basic Auth               │
             │  Authorization: Basic <base64>                 │
             │──────────────────────────────────────────────>│
             │                                                │
             │                  2. Validate Credentialscredentials       │
             │                     -a. Decode Base64           │
             │                     -b. Find token inby DBkey       │
             │                     -c. Check is_active         │
             │                     d. Check expiration        │
             │                     e. Verify integration type │
             │                     f. Verify secret (bcrypt)  │
             │                     -g. Check if active         │
             │                     - Check expiration        │
             │                     - Verify IP (if set)      │
             │                     - Validate scopesrestrictions   │
             │                                                │
             │                  3. Validate scopes            │
             │                     a. Check resource access   │
             │                     b. Check action permission │
             │                                                │
             │                  4. Return Responseresponse            │
             │<──────────────────────────────────────────────│
             │  200 OK + Data (if authorized)authorised)                 │
             │  401 UnauthorizedUnauthorised (if auth fails)              │
             │  403 Forbidden (if scopescope/IP missing)check fails)       │
             │                                                │
             │                  4.5. Update last_used_at        │
             │                     timestamp                  in DB           │
             │                                                

      Validation Steps

       (In Order)

      The server performs the following validationthese checks (insequentially. order):If any step fails, the request is rejected immediately:

      1. Authorization Header: — Checks for Authorization: Basic header
      2. Credential Format: — Validates key:secret format after Base64 decoding
      3. Token Existence: — Looks up token_keythe token key in the database
      4. Active Status: — Verifies is_active flag is true
      5. Expiration: — Checks if token has not expired (expires_at)
      6. Integration Type: — Confirms metadata.integration_type ===is "api"
      7. Secret Verification: — Compares provided secret withagainst the bcrypt hash
      8. IP Restriction: — Validates client IP against allowed_ip_ranges (if set)configured)
      9. Last Used Update — Updates the last_used_at timestamp
      10. Scope Validation: — Verifies the credential has the required scope for the endpoint
      11. Last(handled Usedby Update:separate Updates last_used_at timestampmiddleware)

      Response Codes

      CodeDescriptionReason
      200 OKRequest successfulAuthentication and authorization passed
      401 UnauthorizedAuthentication failedInvalid credentials, expired, or inactive
      403 ForbiddenAuthorization failedValid credentials but insufficient permissions
      500 Internal Server ErrorServer errorUnexpected error during authentication

      Available Scopes & Permissions

      API credentials use a resource-resource–action scope model. Scopes are defined as:model:

      {
        "resource": ["action1", "action2", ...]
      }
      

      Current Resources

      Host Resource

      Resource Namename:: host

      Available Actions:

      (list
      Action DescriptionHTTP MethodsUse Case
      get Read host data GET Retrievehosts, hostview information,details, querystats, hostspackages, network, system, reports, notes, integrations)
      put Replace host data PUTReplace entire host object
      patch UpdatePartially update host dataPATCHPartial updates to host
      updateModify host dataPOST, PATCH General update operations
      delete Delete hosts DELETERemove hosts from system

      Example Scopescope Configurationconfigurations::

      // Read-only access
      { "host": ["get"] }
      
      // Read-onlyRead accessand toupdate
      hosts
      }
      
      { "host": ["get", "patch"] }
      
      // ReadFull andaccess
      update hosts
      }
      
      { "host": ["get", "put", "patch", "update", "delete"] // Full access
      }
      

      ScopeImportant InheritanceNotes

      • Scopes are explicit - no inheritance or wildcards. Each action must be explicitly granted.

      • For example,

      • get permission does not automatically include patch permission.

        or

        Minimumany Scopes

        other

        action.

      • At least one action must be granted for at least one resource. Credentials with no scopes will be rejected during creation.


      API Endpoints

      All endpoints are prefixed with /api/v1/api and require Basic Authentication with a credential that has the appropriate scope.

      Endpoints Summary

      EndpointMethodScopeDescription
      /api/v1/api/hostsGEThost:getList all hosts with IP, groups, and optional stats
      /api/v1/api/hosts/:id/statsGEThost:getGet host package/repo statistics
      /api/v1/api/hosts/:id/infoGEThost:getGet detailed host information
      /api/v1/api/hosts/:id/networkGEThost:getGet host network configuration
      /api/v1/api/hosts/:id/systemGEThost:getGet host system details
      /api/v1/api/hosts/:id/packagesGEThost:getGet host packages (with optional update filter)
      /api/v1/api/hosts/:id/package_reportsGEThost:getGet package update history
      /api/v1/api/hosts/:id/agent_queueGEThost:getGet agent queue status and jobs
      /api/v1/api/hosts/:id/notesGEThost:getGet host notes
      /api/v1/api/hosts/:id/integrationsGEThost:getGet host integration status

      List Hosts

      GetRetrieve a list of all hosts with their IP addresses and host group memberships. Optionally include package update statistics inline with each host.

      Endpoint

      Endpoint:

      GET /api/v1/api/hosts
      

      Required Scope

      Scope:
      {host:get

      "host": ["get"] }

      Query Parameters

      Parameters:

      or
      Parameter Type Required DescriptionExample
      hostgroup string No Filter by host group namesname(s) or UUIDsUUID(s). Comma-separated for multiple groups (comma-separated)OR logic).
      include Production,Developmentstring NoComma-separated list of additional data to include. Supported values: uuid1,uuid2stats.

      Request

      Filtering Headers

      by Host Groups:

      Authorization:# BasicFilter <base64(token_key:token_secret)>by Content-Type:group application/jsonname
      GET /api/v1/api/hosts?hostgroup=Production
      
      # Filter by multiple groups (hosts in ANY of the listed groups)
      GET /api/v1/api/hosts?hostgroup=Production,Development
      
      # Filter by group UUID
      GET /api/v1/api/hosts?hostgroup=550e8400-e29b-41d4-a716-446655440000
      
      # Mix names and UUIDs
      GET /api/v1/api/hosts?hostgroup=Production,550e8400-e29b-41d4-a716-446655440000
      

      Response

      Including Stats:

      Use ?include=stats to add package update counts and additional host metadata to each host in a single request. This is more efficient than making separate /stats calls for every host.

      # List all hosts with stats
      GET /api/v1/api/hosts?include=stats
      
      # Combine with host group filter
      GET /api/v1/api/hosts?hostgroup=Production&include=stats
      

      SuccessNote: If your host group names contain spaces, URL-encode them with %20 (e.g. Web%20Servers). Most HTTP clients handle this automatically.

      Response (200 OK) — Without stats::

      {
        "hosts": [
          {
            "id": "uuid-1234"550e8400-e29b-41d4-a716-446655440000",
            "friendly_name": "web-server-01",
            "hostname": "web01.example.com",
            "ip": "192.168.1.100",
            "host_groups": [
              {
                "id": "group-uuid-1"660e8400-e29b-41d4-a716-446655440001",
                "name": "Production"
              }
            ]
          }
        ],
        "total": 1,
        "filtered_by_groups": ["Production"]
      }
      

      Response (200 OK) — With ?include=stats:

      {
        "hosts": [
          {
            "id": "group-uuid-2",
                "name": "Web Servers"
              }
            ]
          },
          {
            "id": "uuid-5678"550e8400-e29b-41d4-a716-446655440000",
            "friendly_name": "db-web-server-01",
            "hostname": "db01.web01.example.com",
            "ip": "192.168.1.101"100",
            "host_groups": [
              {
                "id": "group-uuid-1"660e8400-e29b-41d4-a716-446655440001",
                "name": "Production"
              }
            ],
            {
                "id"os_type": "group-uuid-3"Ubuntu",
            "name"os_version": "Database24.04 Servers"LTS",
            }"last_update": ]"2026-02-12T10:30:00.000Z",
            "status": "active",
            "needs_reboot": false,
            "updates_count": 15,
            "security_updates_count": 3,
            "total_packages": 342
          }
        ],
        "total": 2,1,
        "filtered_by_groups": ["Production"]
      // Only present if filtering
      }
      

      The filtered_by_groups field is only present when a hostgroup filter is applied.

      Response Fields

      Fields:

      Field Type Description
      hosts array Array of host objects
      hosts[].id string (UUID) Unique host identifier
      hosts[].friendly_name string Human-readable host name
      hosts[].hostname string System hostname
      hosts[].ip string Primary IP address
      hosts[].host_groups array Groups this host belongs to
      hosts[].host_groups[].idos_type string (UUID) GroupOperating identifiersystem type (only with include=stats)
      hosts[].host_groups[].nameos_version string GroupOperating namesystem version (only with include=stats)
      hosts[].last_updatestring (ISO 8601)Timestamp of last agent update (only with include=stats)
      hosts[].statusstringHost status, e.g. active, pending (only with include=stats)
      hosts[].needs_rebootbooleanWhether a reboot is pending (only with include=stats)
      hosts[].updates_countintegerNumber of packages needing updates (only with include=stats)
      hosts[].security_updates_countintegerNumber of security updates available (only with include=stats)
      hosts[].total_packagesintegerTotal installed packages (only with include=stats)
      total integer Total number of hosts returned
      filtered_by_groups array Groups used for filtering (ifonly applicable)present when filtering)

      Filtering
      by

      Get Host Groups

      Statistics

      Retrieve package and repository statistics for a specific host.

      Filter by Group NameEndpoint::

      GET /api/v1/api/hosts?hostgroup=Productionhosts/:id/stats
      

      FilterRequired by Multiple GroupsScope: host:get

      Response (OR200 logicOK):

      -
      {
        hosts"host_id": in"550e8400-e29b-41d4-a716-446655440000",
        ANY"total_installed_packages": of342,
        "outdated_packages": 15,
        "security_updates": 3,
        "total_repos": 8
      }
      

      Response Fields:

      FieldTypeDescription
      host_idstring (UUID)The host identifier
      total_installed_packagesintegerTotal packages installed on this host
      outdated_packagesintegerPackages that need updates
      security_updatesintegerPackages with security updates available
      total_reposintegerTotal repositories associated with the groups):host

      Get Host Information

      Retrieve detailed information about a specific host including OS details and host groups.

      Endpoint:

      GET /api/v1/api/hosts?hostgroup=Production,Developmenthosts/:id/info
      

      FilterRequired byScope: Grouphost:get

      UUID

      Response (200 OK):

      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "machine_id": "abc123def456",
        "friendly_name": "web-server-01",
        "hostname": "web01.example.com",
        "ip": "192.168.1.100",
        "os_type": "Ubuntu",
        "os_version": "24.04 LTS",
        "agent_version": "1.4.0",
        "host_groups": [
          {
            "id": "660e8400-e29b-41d4-a716-446655440001",
            "name": "Production"
          }
        ]
      }
      

      Get Host Network Information

      Retrieve network configuration details for a specific host.

      Endpoint:

      GET /api/v1/api/hosts?hostgroup=550e8400-e29b-41d4-a716-446655440000hosts/:id/network
      

      MixRequired NamesScope: host:get

      Response (200 OK):

      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "ip": "192.168.1.100",
        "gateway_ip": "192.168.1.1",
        "dns_servers": ["8.8.8.8", "8.8.4.4"],
        "network_interfaces": [
          {
            "name": "eth0",
            "ip": "192.168.1.100",
            "mac": "00:11:22:33:44:55"
          }
        ]
      }
      

      Get Host System Information

      Retrieve system-level information for a specific host including hardware, kernel, and UUIDsreboot status.

      Endpoint::

      GET /api/v1/api/hosts?hostgroup=Production,550e8400-e29b-41d4-a716-446655440000hosts/:id/system
      

      ⚠️Required Important:Scope: URLhost:get

      Encoding

      Response for(200 GroupOK):

      Names
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "architecture": "x86_64",
        "kernel_version": "6.8.0-45-generic",
        "installed_kernel_version": "6.8.0-50-generic",
        "selinux_status": "disabled",
        "system_uptime": "15 days, 3:22:10",
        "cpu_model": "Intel Xeon E5-2680 v4",
        "cpu_cores": 4,
        "ram_installed": "8192 MB",
        "swap_size": "2048 MB",
        "load_average": {
          "1min": 0.5,
          "5min": 0.3,
          "15min": 0.2
        },
        "disk_details": [
          {
            "filesystem": "/dev/sda1",
            "size": "50G",
            "used": "22G",
            "available": "28G",
            "use_percent": "44%",
            "mounted_on": "/"
          }
        ],
        "needs_reboot": true,
        "reboot_reason": "Kernel update pending"
      }
      

      Get Host Packages

      Retrieve the list of packages installed on a specific host. Use the optional updates_only parameter to return only packages with Spacesavailable updates.

      Endpoint:

      GET /api/v1/api/hosts/:id/packages
      

      IfRequired your host group names contain spaces, you must URL-encode them usingScope: %20host:get:

      Query Parameters:

      ParameterTypeRequiredDefaultDescription
      updates_onlystringNoSet to true to return only packages that need updates

      Examples:

      # GroupGet name:all packages for a host
      curl -u "Webpatchmon_ae_abc123:your_secret_here" Servers"\
        https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/packages
      
      # Get only packages with available updates
      curl -u "patchmon_ae_abc123:your_secret_here" \
        "https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/packages?updates_only=true"
      

      Response (200 OK):

      {
        "host": {
          "id": "550e8400-e29b-41d4-a716-446655440000",
          "hostname": "web01.example.com",
          "friendly_name": "web-server-01"
        },
        "packages": [
          {
            "id": "package-host-uuid",
            "name": "nginx",
            "description": "High performance web server",
            "category": "web",
            "current_version": "1.18.0-0ubuntu1.5",
            "available_version": "1.24.0-2ubuntu1",
            "needs_update": true,
            "is_security_update": false,
            "last_checked": "2026-02-12T10:30:00.000Z"
          },
          {
            "id": "package-host-uuid-2",
            "name": "openssl",
            "description": "Secure Sockets Layer toolkit",
            "category": "security",
            "current_version": "3.0.2-0ubuntu1.14",
            "available_version": "3.0.2-0ubuntu1.18",
            "needs_update": true,
            "is_security_update": true,
            "last_checked": "2026-02-12T10:30:00.000Z"
          }
        ],
        "total": 2
      }
      

      Response Fields:

      FieldTypeDescription
      hostobjectBasic host identification
      host.idstring (UUID)Host identifier
      host.hostnamestringSystem hostname
      host.friendly_namestringHuman-readable host name
      packagesarrayArray of package objects
      packages[].idstring (UUID)Host-package record identifier
      packages[].namestringPackage name
      packages[].descriptionstringPackage description
      packages[].categorystringPackage category
      packages[].current_versionstringCurrently installed version
      packages[].available_versionstring | nullAvailable update version (null if up to date)
      packages[].needs_updatebooleanWhether an update is available
      packages[].is_security_updatebooleanWhether the available update is security-related
      packages[].last_checkedstring (ISO 8601)When this package was last checked
      totalintegerTotal number of packages returned

      Tip: Packages are returned sorted by security updates first, then by update availability. This puts the most critical packages at the top.


      Get Host Package Reports

      Retrieve package update history reports for a specific host.

      Endpoint:

      GET /api/v1/api/hosts?hostgroup=Web%20Servershosts/:id/package_reports
      #
      Group

      Required name:Scope: host:get

      Query Parameters:

      ParameterTypeRequiredDefaultDescription
      limitintegerNo10Maximum number of reports to return

      Response (200 OK):

      {
        "Productionhost_id": Database""550e8400-e29b-41d4-a716-446655440000",
        "reports": [
          {
            "id": "report-uuid",
            "status": "success",
            "date": "2026-02-12T10:30:00.000Z",
            "total_packages": 342,
            "outdated_packages": 15,
            "security_updates": 3,
            "payload_kb": 12.5,
            "execution_time_seconds": 4.2,
            "error_message": null
          }
        ],
        "total": 1
      }
      

      Get Host Agent Queue

      Retrieve agent queue status and job history for a specific host.

      Endpoint:

      GET /api/v1/api/hosts?hostgroup=Production%20Databasehosts/:id/agent_queue
      #
      Multiple

      Required groupsScope: host:get

      Query Parameters:

      ParameterTypeRequiredDefaultDescription
      limitintegerNo10Maximum number of jobs to return

      Response (200 OK):

      {
        "host_id": "550e8400-e29b-41d4-a716-446655440000",
        "queue_status": {
          "waiting": 0,
          "active": 1,
          "delayed": 0,
          "failed": 0
        },
        "job_history": [
          {
            "id": "job-history-uuid",
            "job_id": "bull-job-id",
            "job_name": "package_update",
            "status": "completed",
            "attempt": 1,
            "created_at": "2026-02-12T10:00:00.000Z",
            "completed_at": "2026-02-12T10:05:00.000Z",
            "error_message": null,
            "output": null
          }
        ],
        "total_jobs": 1
      }
      

      Get Host Notes

      Retrieve notes associated with spacesa specific host.

      Endpoint:

      GET /api/v1/api/hosts?hostgroup=Web%20Servers,Database%20Servershosts/:id/notes
      

      MostRequired HTTP clients handle this automatically, but if constructing URLs manually, remember to replace spaces withScope: %20host:get

      Response (200 OK):

      {
        "host_id": "550e8400-e29b-41d4-a716-446655440000",
        "notes": "Production web server. Enrolled via Proxmox auto-enrollment on 2026-01-15."
      }
      

      Get Host Integrations

      Retrieve integration status and details for a specific host (e.g. Docker).

      Endpoint:

      GET /api/v1/api/hosts/:id/integrations
      

      Required Scope: host:get

      Response (200 OK) — Docker enabled:

      {
        "host_id": "550e8400-e29b-41d4-a716-446655440000",
        "integrations": {
          "docker": {
            "enabled": true,
            "containers_count": 12,
            "volumes_count": 5,
            "networks_count": 3,
            "description": "Monitor Docker containers, images, volumes, and networks. Collects real-time container status events."
          }
        }
      }
      

      Response (200 OK) — Docker not enabled:

      {
        "host_id": "550e8400-e29b-41d4-a716-446655440000",
        "integrations": {
          "docker": {
            "enabled": false,
            "description": "Monitor Docker containers, images, volumes, and networks. Collects real-time container status events."
          }
        }
      }
      

      Common Error Responses

       (All Endpoints)

      401404 UnauthorizedNot Found - InvalidHost ordoes expirednot credentials:exist (for single-host endpoints):

      {
        "error": "Invalid API key"
      }
      

      401 Unauthorized - Inactive credential:

      {
        "error": "API key is disabled"
      }
      

      401 Unauthorized - Expired credential:

      {
        "error": "API key has expired"
      }
      

      403 Forbidden - Invalid IP address:

      {
        "error": "IP addressHost not allowed"
      }
      

      403 Forbidden - Missing scope:

      {
        "error": "Access denied",
        "message": "This API key does not have permission to get host"found"
      }
      

      500 Internal Server Error - ServerUnexpected server error:

      {
        "error": "Failed to fetch hosts"
      }
      

      See the Troubleshooting section for authentication and permission errors.


      Usage Examples

      cURL Examples

      Basic Request - List All Hosts

      curl -u "patchmon_api_abc123:def456xyz789"patchmon_ae_abc123:your_secret_here" \
        https://patchmon.example.com/api/v1/api/hosts
      

      List Hosts with Stats

      curl -u "patchmon_ae_abc123:your_secret_here" \
        "https://patchmon.example.com/api/v1/api/hosts?include=stats"
      

      Filter by SingleHost Group

      curl -u "patchmon_api_abc123:def456xyz789"patchmon_ae_abc123:your_secret_here" \
        "https://patchmon.example.com/api/v1/api/hosts?hostgroup=Production"
      

      Filter by Host Group with Stats

      curl -u "patchmon_ae_abc123:your_secret_here" \
        "https://patchmon.example.com/api/v1/api/hosts?hostgroup=Production&include=stats"
      

      Filter by Multiple Groups

      curl -u "patchmon_api_abc123:def456xyz789"patchmon_ae_abc123:your_secret_here" \
        "https://patchmon.example.com/api/v1/api/hosts?hostgroup=Production,Development"
      

      FilterGet byHost Group with Spaces in NameStatistics

      # Group name: "Web Servers" - use %20 for spaces
      curl -u "patchmon_api_abc123:def456xyz789"patchmon_ae_abc123:your_secret_here" \
        https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/stats
      

      Get Host System Information

      curl -u "patchmon_ae_abc123:your_secret_here" \
        https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/system
      

      Get All Packages for a Host

      curl -u "patchmon_ae_abc123:your_secret_here" \
        https://patchmon.example.com/api/v1/api/hosts/HOST_UUID/packages
      

      Get Only Packages with Available Updates

      curl -u "patchmon_ae_abc123:your_secret_here" \
        "https://patchmon.example.com/api/v1/api/hosts?hostgroup=Web%20Servers"hosts/HOST_UUID/packages?updates_only=true"
      

      Pretty Print JSON Output

      curl -u "patchmon_api_abc123:def456xyz789"patchmon_ae_abc123:your_secret_here" \
        https://patchmon.example.com/api/v1/api/hosts | jq .
      

      Save Response to File

      curl -u "patchmon_api_abc123:def456xyz789" \
        https://patchmon.example.com/api/v1/api/hosts \
        -o hosts.json
      

      Verbose Output (See Headers)

      curl -v -u "patchmon_api_abc123:def456xyz789" \
        https://patchmon.example.com/api/v1/api/hosts
      

      Python Examples

      Using requests Library

      import requests
      from requests.auth import HTTPBasicAuth
      
      # API credentials
      API_KEY = "patchmon_api_abc123"patchmon_ae_abc123"
      API_SECRET = "def456xyz789"your_secret_here"
      BASE_URL = "https://patchmon.example.com"
      
      # Create session with authentication
      session = requests.Session()
      session.auth = HTTPBasicAuth(API_KEY, API_SECRET)
      
      # GetList all hosts
      response = session.get(f"{BASE_URL}/api/v1/api/hosts")
      
      if response.status_code == 200:
          data = response.json()
          print(f"Total hosts: {data['total']}")
      
          for host in data['hosts']:
              print(f"Host:groups {host['friendly_name']}= ({host['ip']})")
              print(f"  Groups: {', '.join([g['name'] for g in host['host_groups']])
              print(f"  {host['friendly_name']} ({host['ip']}) — Groups: {groups}")
      else:
          print(f"Error: {response.status_code} - {response.json()}")
      

      Filter by Host Group

      # GetFilter hosts in Productionby group response = session.get(
          f"{BASE_URL}/api/v1/api/hosts",
          params={"hostgroup": "Production"}
      )
      
      if response.status_code == 200:
          data = response.json()
          print(f"Production hosts: {data['total']}")
      
      # Get hosts in group with spacesname (requests handles URL encoding automatically)
      response = session.get(
          f"{BASE_URL}/api/v1/api/hosts",
          params={"hostgroup": "WebProduction"}
      Servers")
      

      List Hosts with Inline Stats

      # Get hosts with stats in a single request (more efficient than per-host /stats calls)
      response = session.get(
          f"{BASE_URL}/api/v1/api/hosts",
          params={"include": "stats"}
      )
      
      if response.status_code == 200:
          data = response.json()
          for host in data['hosts']:
              print(f"{host['friendly_name']}: {host['updates_count']} updates, "
                    f"{host['security_updates_count']} security, "
                    f"{host['total_packages']} total packages")
      

      Get Host Packages (Updates Only)

      # Get only packages that need updates for a specific host
      response = session.get(
          f"{BASE_URL}/api/v1/api/hosts/{host_id}/packages",
          params={"updates_only": "true"}
      )
      
      if response.status_code == 200:
          data = response.json()
          print(f"Host: {data['host']['friendly_name']}")
          print(f"Packages needing updates: {data['total']}")
          for pkg in data['packages']:
              security = " [SECURITY]" if pkg['is_security_update'] else ""
              print(f"  {pkg['name']}: {pkg['current_version']} → {pkg['available_version']}{security}")
      

      Get Host Details and Stats

      # First, get list of hosts
      hosts_response = session.get(f"{BASE_URL}/api/v1/api/hosts")
      hosts = hosts_response.json()['hosts']
      
      # AutomaticallyThen encodedget tostats Web%20Serversfor the first host
      if hosts:
          host_id = hosts[0]['id']
      
          stats = session.get(f"{BASE_URL}/api/v1/api/hosts/{host_id}/stats").json()
          print(f"Installed: {stats['total_installed_packages']}")
          print(f"Outdated: {stats['outdated_packages']}")
          print(f"Security: {stats['security_updates']}")
      
          info = session.get(f"{BASE_URL}/api/v1/api/hosts/{host_id}/info").json()
          print(f"OS: {info['os_type']} {info['os_version']}")
          print(f"Agent: {info['agent_version']}")
      

      Error Handling

      def get_hosts(hostgroup=None):
          """Get hosts with error handling"handling."""
          try:
              params = {"hostgroup": hostgroup} if hostgroup else {}
              response = session.get(
                  f"{BASE_URL}/api/v1/api/hosts",
                  params=params,
                  timeout=30
              )
              response.raise_for_status()
              return response.json()
      
          except requests.exceptions.HTTPError as e:
              if e.response.status_code == 401:
                  print("Authentication failed - check credentials")
              elif e.response.status_code == 403:
                  print("Access denied - insufficient permissions")
              else:
                  print(f"HTTP error: {e}")
              return None
      
          except requests.exceptions.Timeout:
              print("Request timed out")
              return None
      
          except requests.exceptions.RequestException as e:
              print(f"Request failed: {e}")
              return None
      # Usage
      hosts = get_hosts(hostgroup="Production")
      if hosts:
          for host in hosts['hosts']:
              print(f"{host['friendly_name']}: {host['ip']}")
      

      Generate Ansible Inventory

      import json
      import requests
      from requests.auth import HTTPBasicAuth
      
      API_KEY = "patchmon_api_abc123"patchmon_ae_abc123"
      API_SECRET = "def456xyz789"your_secret_here"
      BASE_URL = "https://patchmon.example.com"
      
      def generate_ansible_inventory():
          """Generate Ansible inventory from PatchMon hosts"hosts.""
          
          # Authenticate"
          auth = HTTPBasicAuth(API_KEY, API_SECRET)
          response = requests.get(f"{BASE_URL}/api/v1/api/hosts", auth=auth)auth, timeout=30)
      
          if response.status_code != 200:
              print(f"Error fetching hosts: {response.status_code}")
              return
      
          data = response.json()
      
          # Build Ansible inventory structure
          inventory = {
              "_meta": {
                  "hostvars": {}
              },
              "all": {
                  "hosts": [], "children": []
              }
          }
          
          # Process hosts
      
          for host in data['hosts']:
              hostname = host['friendly_name']
              inventory["all"]["hosts"].append(hostname)
              
              # Add host variables
      
              inventory["_meta"]["hostvars"][hostname] = {
                  "ansible_host": host['ip'],
                  "patchmon_id": host['id'],
                  "patchmon_hostname": host['hostname']
              }
              
              # Add to groups
      
              for group in host['host_groups']:
                  group_name = group['name'].lower().replace(' ', '_')
      
                  if group_name not in inventory:
                      inventory[group_name] = {
                          "hosts": [], "vars": {}
                      }
                      inventory["all"]["children"].append(group_name)
      
                  inventory[group_name]["hosts"].append(hostname)
          
          # Output inventory
      
          print(json.dumps(inventory, indent=2))
      
      if __name__ == "__main__":
          generate_ansible_inventory()
      

      JavaScript/Node.js Examples

      Using Native axiosfetch (Node.js 18+)

      const axios = require('axios');
      
      // API credentials
      const API_KEY = 'patchmon_api_abc123'patchmon_ae_abc123';
      const API_SECRET = 'def456xyz789'your_secret_here';
      const BASE_URL = 'https://patchmon.example.com';
      
      // Create axios instance with authentication
      const client = axios.create({
        baseURL: BASE_URL,
        auth: {
          username: API_KEY,
          password: API_SECRET
        }
      });
      
      // Get all hosts
      async function getAllHosts() {
        try {
          const response = await client.get('/api/v1/api/hosts');
          const { hosts, total } = response.data;
          
          console.log(`Total hosts: ${total}`);
          
          hosts.forEach(host => {
            console.log(`Host: ${host.friendly_name} (${host.ip})`);
            const groups = host.host_groups.map(g => g.name).join(', ');
            console.log(`  Groups: ${groups}`);
          });
          
          return hosts;
        } catch (error) {
          if (error.response) {
            // Server responded with error
            console.error(`Error: ${error.response.status} - ${error.response.data.error}`);
          } else if (error.request) {
            // Request made but no response
            console.error('No response from server');
          } else {
            // Error setting up request
            console.error(`Error: ${error.message}`);
          }
          throw error;
        }
      }
      
      // Get hosts by group
      async function getHostsByGroup(groupName) {
        try {
          const response = await client.get('/api/v1/api/hosts', {
            params: { hostgroup: groupName }  // axios handles URL encoding automatically
          });
          
          return response.data.hosts;
        } catch (error) {
          console.error(`Failed to get hosts for group ${groupName}:`, error.message);
          throw error;
        }
      }
      
      // Usage
      (async () => {
        try {
          // Get all hosts
          await getAllHosts();
          
          // Get production hosts
          const productionHosts = await getHostsByGroup('Production');
          console.log(`Production hosts: ${productionHosts.length}`);
        } catch (error) {
          console.error('Failed to fetch hosts');
          process.exit(1);
        }
      })();
      

      Using Native fetch (Node.js 18+)

      // API credentials
      const API_KEY = 'patchmon_api_abc123';
      const API_SECRET = 'def456xyz789';
      const BASE_URL = 'https://patchmon.example.com';
      
      // Create Basic Auth header
      
      const authHeader = 'Basic ' + Buffer.from(`${API_KEY}:${API_SECRET}`).toString('base64');
      
      async function getHosts(hostgroup = null) {
        const url = new URL('/api/v1/api/hosts', BASE_URL);
        if (hostgroup) {
          url.searchParams.append('hostgroup', hostgroup);
        // URL class handles encoding automatically
        }
        
        try {
      
        const response = await fetch(url, {
          headers: {
            'Authorization': authHeader,
            'Content-Type': 'application/json'
          }
        });
      
        if (!response.ok) {
          const error = await response.json();
          throw new Error(`HTTP ${response.status}: ${error.error}`);
        }
      
        return await response.json();
      }
      
      catch (error) {
          console.error('Failed to fetch hosts:', error.message);
          throw error;
        }
      }
      
      // UsageList all hosts
      getHosts('Production')
        .then(data => {
          console.log(`Total: ${data.total}`);
          data.hosts.forEach(host => {
            console.log(`${host.friendly_name}: ${host.ip}`);
          });
        })
        .catch(error => {
          console.error('Error:', error.message);
        });
      
      // Group with spaces - URL encoding handled automatically
      getHosts('Web Servers')
        .then(data => {
          console.log(`Web Servers total: ${data.total}`);
        });
      

      Ansible Integration

      Dynamic Inventory Script

      Save this as patchmon_inventory.py and make it executable:executable (chmod +x):

      #!/usr/bin/env python3
      """
      PatchMon Dynamic Inventory Script for AnsibleAnsible.
      Usage: ansible-playbook -i patchmon_inventory.py playbook.yml
      """
      
      import json
      import os
      import sys
      import requests
      from requests.auth import HTTPBasicAuth
      
      # Configuration from environment variables
      
      API_KEY = os.environ.get('PATCHMON_API_KEY')
      API_SECRET = os.environ.get('PATCHMON_API_SECRET')
      BASE_URL = os.environ.get('PATCHMON_URL', 'https://patchmon.example.com')
      
      if not API_KEY or not API_SECRET:
          print("Error: PATCHMON_API_KEY and PATCHMON_API_SECRET must be set", file=sys.stderr)
          sys.exit(1)
      
      def get_inventory():
          """Fetch inventory from PatchMon API"""
          auth = HTTPBasicAuth(API_KEY, API_SECRET)
          try:
              response = requests.get(
                  f"{BASE_URL}/api/v1/api/hosts", auth=auth, timeout=30
              )30)
              response.raise_for_status()
              return response.json()
          except requests.exceptions.RequestException as e:
              print(f"Error fetching inventory: {e}", file=sys.stderr)
              sys.exit(1)
      
      def build_ansible_inventory(patchmon_data):
          """Convert PatchMon data to Ansible inventory format"""
          inventory = {
              "_meta": {
                  "hostvars": {}
              },
              "all": {
                  "hosts": []
              }
          }
          groups = {}
      
          for host in patchmon_data['hosts']:
              hostname = host['friendly_name']
              # Add to all hosts
              inventory["all"]["hosts"].append(hostname)
              
              # Set host variables
      
              inventory["_meta"]["hostvars"][hostname] = {
                  "ansible_host": host['ip'],
                  "patchmon_id": host['id'],
                  "patchmon_hostname": host['hostname']
              }
              
              # Process groups
      
              for group in host['host_groups']:
                  group_name = group['name'].lower().replace(' ', '_').replace('-', '_')
                  if group_name not in groups:
                      groups[group_name] = {
                          "hosts": [],
                          "vars": {
                              "patchmon_group_id": group['id']
                          }
                      }
                  groups[group_name]["hosts"].append(hostname)
      
          # Add groups to inventory
          inventory.update(groups)
          return inventory
      
      def main():
          """Main entry point"""
          # Ansible passes --list or --host <hostname>
          if len(sys.argv) == 2 and sys.argv[1] == '--list':
              # Return full inventory
              patchmon_data = get_inventory()
              inventory = build_ansible_inventory(patchmon_data)
              print(json.dumps(inventory, indent=2))
          elif len(sys.argv) == 3 and sys.argv[1] == '--host':
              # Return empty dict (we use _meta for hostvars)
              print(json.dumps({}))
          else:
              print("Usage: patchmon_inventory.py --list", file=sys.stderr)
              sys.exit(1)
      
      if __name__ == '__main__':
          main()
      

      Usage with Ansible

      Usage:

      # Export credentials
      export PATCHMON_API_KEY="patchmon_api_abc123"patchmon_ae_abc123"
      export PATCHMON_API_SECRET="def456xyz789"your_secret_here"
      export PATCHMON_URL="https://patchmon.example.com"
      
      # Make script executable
      chmod +x patchmon_inventory.py
      
      # Test inventory
      ./patchmon_inventory.py --list
      
      # Use with ansible
      ansible-playbook -i patchmon_inventory.py playbook.yml
      # Use with ansible ad-hoc commands
      ansible -i patchmon_inventory.py production -m ping
      ansible -i patchmon_inventory.py all -m setup
      

      Example Playbook

      ---
      - name: Update all PatchMon hosts
        hosts: all
        gather_facts: yes
        become: yes
        
        tasks:
          - name: Display host info
            debug:
              msg: "Host {{ inventory_hostname }} ({{ ansible_host }}) - PatchMon ID: {{ patchmon_id }}"
          
          - name: Update package cache
            apt:
              update_cache: yes
            when: ansible_os_family == "Debian"
          
          - name: Upgrade all packages
            apt:
              upgrade: dist
            when: ansible_os_family == "Debian"
      
      - name: Production-specific tasks
        hosts: production
        gather_facts: no
        become: yes
        
        tasks:
          - name: Restart services after update
            systemd:
              name: "{{ item }}"
              state: restarted
            loop:
              - nginx
              - php-fpm
      

      Expected Outputs

      Successful Request - All Hosts

      Request:

      curl -u "patchmon_api_abc123:def456xyz789" \
        https://patchmon.example.com/api/v1/api/hosts
      

      Response (HTTP 200):

      {
        "hosts": [
          {
            "id": "550e8400-e29b-41d4-a716-446655440000",
            "friendly_name": "web-server-01",
            "hostname": "web01.example.com",
            "ip": "192.168.1.100",
            "host_groups": [
              {
                "id": "660e8400-e29b-41d4-a716-446655440001",
                "name": "Production"
              },
              {
                "id": "770e8400-e29b-41d4-a716-446655440002",
                "name": "Web Servers"
              }
            ]
          },
          {
            "id": "550e8400-e29b-41d4-a716-446655440003",
            "friendly_name": "db-server-01",
            "hostname": "db01.example.com",
            "ip": "192.168.1.101",
            "host_groups": [
              {
                "id": "660e8400-e29b-41d4-a716-446655440001",
                "name": "Production"
              },
              {
                "id": "880e8400-e29b-41d4-a716-446655440004",
                "name": "Database Servers"
              }
            ]
          },
          {
            "id": "550e8400-e29b-41d4-a716-446655440005",
            "friendly_name": "web-server-dev-01",
            "hostname": "webdev01.example.com",
            "ip": "192.168.2.100",
            "host_groups": [
              {
                "id": "990e8400-e29b-41d4-a716-446655440006",
                "name": "Development"
              },
              {
                "id": "770e8400-e29b-41d4-a716-446655440002",
                "name": "Web Servers"
              }
            ]
          }
        ],
        "total": 3
      }
      

      Successful Request - Filtered by Group

      Request:

      curl -u "patchmon_api_abc123:def456xyz789" \
        "https://patchmon.example.com/api/v1/api/hosts?hostgroup=Production"
      

      Response (HTTP 200):

      {
        "hosts": [
          {
            "id": "550e8400-e29b-41d4-a716-446655440000",
            "friendly_name": "web-server-01",
            "hostname": "web01.example.com",
            "ip": "192.168.1.100",
            "host_groups": [
              {
                "id": "660e8400-e29b-41d4-a716-446655440001",
                "name": "Production"
              },
              {
                "id": "770e8400-e29b-41d4-a716-446655440002",
                "name": "Web Servers"
              }
            ]
          },
          {
            "id": "550e8400-e29b-41d4-a716-446655440003",
            "friendly_name": "db-server-01",
            "hostname": "db01.example.com",
            "ip": "192.168.1.101",
            "host_groups": [
              {
                "id": "660e8400-e29b-41d4-a716-446655440001",
                "name": "Production"
              },
              {
                "id": "880e8400-e29b-41d4-a716-446655440004",
                "name": "Database Servers"
              }
            ]
          }
        ],
        "total": 2,
        "filtered_by_groups": ["Production"]
      }
      

      Empty Result - No Hosts Found

      Request:

      curl -u "patchmon_api_abc123:def456xyz789" \
        "https://patchmon.example.com/api/v1/api/hosts?hostgroup=NonExistentGroup"
      

      Response (HTTP 200):

      {
        "hosts": [],
        "total": 0,
        "filtered_by_groups": ["NonExistentGroup"]
      }
      

      Error - Invalid Credentials

      Request:

      curl -u "invalid_key:invalid_secret" \
        https://patchmon.example.com/api/v1/api/hosts
      

      Response (HTTP 401):

      {
        "error": "Invalid API key"
      }
      

      Error - Expired Credential

      Response (HTTP 401):

      {
        "error": "API key has expired"
      }
      

      Error - Insufficient Permissions

      Request: Credential with no host:get scope

      Response (HTTP 403):

      {
        "error": "Access denied",
        "message": "This API key does not have permission to get host"
      }
      

      Error - IP Not Allowed

      Request: From IP not in allowed_ip_ranges

      Response (HTTP 403):

      {
        "error": "IP address not allowed"
      }
      

      Error - Inactive Credential

      Response (HTTP 401):

      {
        "error": "API key is disabled"
      }ping
      

      Security Best Practices

      Credential Management

      1. Store Credentials Securely

      ✅ DO:Do:

      • UseStore credentials in a password manager (1Password, LastPass, Bitwarden)
      • Store inor secrets management systemsvault (e.g. HashiCorp Vault, AWS Secrets Manager)
      • Use environment variables for automation scripts
      • EncryptSet expiration dates (recommended: 90 days)
      • Grant only the minimum permissions needed (principle of least privilege)
      • Rotate credentials atregularly restand delete old ones after migration

      ❌ DON'T:Don't:

      • Hard-code credentials in source code
      • Commit credentials to version control
      • Share credentials via email or chat
      • Store credentials in plain plain-text files

      2. Use Environment Variables

      # ~/.bashrc or ~/.zshrc
      export PATCHMON_API_KEY="patchmon_api_abc123"
      export PATCHMON_API_SECRET="def456xyz789"
      export PATCHMON_URL="https://patchmon.example.com"
      
      # Python
      import os
      API_KEY = os.environ.get('PATCHMON_API_KEY')
      API_SECRET = os.environ.get('PATCHMON_API_SECRET')
      
      // Node.js
      const API_KEY = process.env.PATCHMON_API_KEY;
      const API_SECRET = process.env.PATCHMON_API_SECRET;
      

      3. Rotate Credentials Regularly

      • Set expiration dates (recommended: 90 days)
      • Create new credentials before old ones expire
      • Update all systems using the credential
      • Delete old credentials after migration

      4. Use Least Privilege

      Grant only the minimum permissions needed:

      // Read-only inventory access
      {
        "host": ["get"]
      }
      
      // Update hosts but no delete
      {
        "host": ["get", "patch"]
      }
      

      5. Implement

      IP Restrictions

      Restrict credentials to known IP addresses:addresses whenever possible:

      Allowed IPs: 192.168.1.100, 10.0.0.500/24
      

      For dynamic IPs, consider:

      consider
        using
      • a VPN with a static exit IPs
      • IP,
      • Clouda providercloud NAT gatewaygateway, IPs
      • or
      • Proxya serversproxy with static IPs
      server.

      Network Security

      1.
      • Always Useuse HTTPS

      in DO:

      production
      curlenvironments
      -u "key:secret" https://patchmon.example.com/api/v1/api/hosts
      

      ❌ DON'T:

      curl -u "key:secret" http://patchmon.example.com/api/v1/api/hosts
      

      2.
    11. Verify SSL Certificatescertificates
    12. # Default (verifies SSL)
      curl -u "key:secret" https://patchmon.example.com/api/v1/api/hosts
      
      # Onlyonly disable verification (-k) for testing/developmentdevelopment/testing
      curl -k -u "key:secret" https://patchmon.example.com/api/v1/api/hosts
      
      # Python - verify SSL
      requests.get(url, auth=auth, verify=True)  # Default
      
      # Never in production!
      requests.get(url, auth=auth, verify=False)  # Insecure
      

      3.
    13. Use Firewallfirewall Rulesrules
    14. Restrictto restrict PatchMon API access at the network level:

      level
      # Allow only specific IPs
      iptables -A INPUT -p tcp --dport 443 -s 192.168.1.100 -j ACCEPT
      iptables -A INPUT -p tcp --dport 443 -j DROP
      

      Monitoring & Auditing

      1. Monitor Credential Usage

      • Check "Last Used" timestamps regularly in the Integrations settings page
      • Investigate credentials that haven'thave not been used in 30+ days
      • Look for unusual access patterns

      2. Set Up Alerts

      Monitor for:

      • Failed authentication attempts
      • Access from unexpected IPs
      • Disabled credentials still being used
      • Expired credentials being used

      3. Regular Audits

      • Review all active credentials monthly
      • Remove credentials for decommissioned systems
      • Verify credential owners are still valid
      • Check scope assignments are still appropriate

      Incident Response

      If Credentials Are Compromised:

      Compromised
      1. Immediately disable the credential in PatchMon UI
      2. Review "Last Used" timestamp to understand exposure
      3. Check server logs for unauthorized access
      4. Create new credentials with different scope if needed
      5. Delete compromised credentials after verification
      6. Notify security team if data was accessed
      7. Update incident response documentation

      Troubleshooting

      Authentication Issues

      Problem: "Invalid API key"

      Possible Causes:

      • Token key is incorrect
      • Token has been deleted
      • Token key format is invalid

      Solution:

      1. Verify the token key is correct (check for copy/paste errors)
      2. Confirm the credential exists in PatchMon UI
      3. Check for extra whitespace or special characters
      4. Try creating a new credential and testing

      Problem: "Invalid API secret"

      Possible Causes:

      • Token secret is incorrect
      • Secret was copied incorrectly

      Solution:

      1. Secrets cannot be retrieved - create a new credential
      2. Ensure no trailing spaces or newlines in secret
      3. Verify the entire secret was copied

      Problem: "API key is disabled"

      Possible Causes:

      • Credential was manually disabled
      • Security incident response

      Solution:

      1. Check credential status in PatchMon UI (Settings → Integrations → API)toggle off)
      2. ClickReview the "Enable"Last Used" timestamp to reactivateunderstand the window of exposure
      3. OrCheck createserver alogs newfor credentialany ifunauthorised this one should remain disabled

      Problem: "API key has expired"

      Possible Causes:

      • Expiration date has passed

      Solution:

      1. Create a new credential to replace the expired oneaccess
      2. Update your systems with theCreate new credentials
      3. Deletewith thea old credential

      Permission Issues

      Problem: "Access denied"

      Possible Causes:

      • Credential doesn't have requireddifferent scope
      • Wrong integration type

      Solution:

      1. Check the error message for specific permissionif needed
      2. GoDelete tothe Settingscompromised credential Integrationsafter → APIverification
      3. ClickNotify "Edit"your onsecurity theteam credential
      4. if
      5. Addsensitive thedata requiredmay scopehave (e.g.,been host: get)
      6. Save and retry the requestaccessed

      Problem:
      "This API key does not have permission to get host"

      Solution: Edit the credential and add the get action to the host resource scope.

      Troubleshooting

      Network Issues

      Problem: "IP address not allowed"

      Possible Causes:

      • Request is coming from an IP not in allowed_ip_ranges
      • Proxy or load balancer changing source IP

      Solution:

      1. Identify your actual source IP:
        curl https://ifconfig.me
        
      2. Add this IP to the credential's allowed IP ranges
      3. If behind a proxy/NAT, add the proxy's IP
      4. Consider removing IP restrictions for development

      Problem: Connection timeout

      Possible Causes:

      • Network connectivity issues
      • Firewall blocking access
      • PatchMon server is down

      Solution:

      1. Test basic connectivity:
        ping patchmon.example.com
        curl -I https://patchmon.example.com/health
        
      2. Check firewall rules
      3. Verify PatchMon server status
      4. Try from a different network

      Problem: SSL certificate errors

      Possible Causes:

      • Self-signed certificate
      • Expired certificate
      • Certificate name mismatch

      Solution:

      1. For production: Fix the SSL certificate
      2. For development only:
        curl -k ...  # Skip verification (insecure)
        
      3. Python:
        requests.get(url, auth=auth, verify=False)  # Development only
        

      Response Issues

      Problem: Empty hosts array

      Possible Causes:

      • No hosts in the system
      • Filtering by non-existent group
      • No hosts in the specified group

      Solution:

      1. Verify hosts exist: Check PatchMon UI → Hosts
      2. Check filter spelling:
        # Wrong
        ?hostgroup=Productin
        
        # Correct
        ?hostgroup=Production
        
      3. List all hosts without filters to verify API access

      Problem: Unexpected response format

      Possible Causes:

      • Wrong API endpoint
      • API version mismatch
      • Server error returning HTML

      Solution:

      1. Verify endpoint URL is correct
      2. Check API version in URL (/api/v1/...)
      3. Add verbose output to see full response:
        curl -v ...
        
      4. Check Content-Type header is application/json

      Common ErrorsError Reference

      Error Message HTTP Code Cause Solution
      "Missing or invalid authorization header"header 401 No Authorization headerheader, or wrongit formatdoesn't start with Basic Use -u key:secret with cURLcURL, or set Authorization: Basic <base64> header
      "Invalid credentials format"format 401 AuthorizationBase64-decoded headervalue doesn't contain key:secreta colon separator Check format is key:secret with colonensure no extra characters
      "Invalid API key"key 401 Token key not found in the database Verify the credential exists andin keySettings is correctIntegrations
      "API key is disabled"disabled 401 Credential inactivehas been manually deactivated EnableRe-enable credentialin Settings → Integrations, or create new one
      "API key has expired"401Past expiration dateCreatea new credential
      "Invalid API key type"has expired 401 NotThe anexpiration APIdate integrationhas typepassed CredentialCreate musta benew typecredential "api"to replace the expired one
      "Invalid API secret"key type401The credential's integration_type is not "api"Ensure you created the credential with the "API" usage type
      Invalid API secret 401 Secret doesn't match the stored bcrypt hash RecreateCreate a new credential (secrets cannot be retrieved)
      "IP address not allowed"allowed 403 SourceClient IP is not in allowedthe rangescredential's allowed_ip_ranges Add IPyour IP: curl https://ifconfig.me to allowedfind rangesit
      "Access denied"denied — does not have permission to {action} {resource} 403 MissingCredential is missing the required scope AddEdit the credential and add the required permission to credential
      "Access denied — does not have access to {resource}403The resource is not included in the credential's scopes at allEdit the credential's scopes to include the resource
      Host not found404The host UUID does not existVerify the UUID from the list hosts endpoint
      Failed to fetch hosts"hosts 500 ServerUnexpected server error Check PatchMon server logs,logs contactfor supportdetails
      Authentication failed500Unexpected error during authentication processingCheck PatchMon server logs; may indicate a database issue

      Debug ModeTips

      EnablecURL verbose output for troubleshooting:mode:

      cURL

      # Verbose mode - shows full request/response
      curl -v -u "key:secret"patchmon_ae_abc123:your_secret_here" https://patchmon.example.com/api/v1/api/hosts
      
      # Trace mode - even more detailed
      curl --trace-ascii - -u "key:secret"\
        https://patchmon.example.com/api/v1/api/hosts
      

      Python

       debug logging:

      import logging
      import requests
      
      # Enable debug logging
      logging.basicConfig(level=logging.DEBUG)
      requests_log = logging.getLogger("requests.packages.urllib3")
      requests_log.setLevel(logging.DEBUG)
      requests_log.propagate = True
      

      Common Issues

      Empty hosts array

      • Verify hosts exist in PatchMon UI → Hosts page
      • Check the hostgroup filter spelling matches exactly (case-sensitive)
      • Try listing all hosts without filters first to confirm API access works

      Connection timeouts

      # MakeTest requestbasic connectivity
      ping patchmon.example.com
      curl -I will show full details
      response = requests.get(url, auth=auth)https://patchmon.example.com/health
      

      JavaScriptSSL certificate errors

      For development/testing with self-signed certificates:

      constcurl axios-k =-u require('axios');"patchmon_ae_abc123:your_secret_here" \
        https:// Enable debug mode
      axios.defaults.debug = true;
      
      // Or use interceptors
      axios.interceptors.request.use(request => {
        console.log('Request:', JSON.stringify(request, null, 2));
        return request;
      });
      
      axios.interceptors.response.use(response => {
        console.log('Response:', JSON.stringify(response.data, null, 2));
        return response;
      });patchmon.example.com/api/v1/api/hosts
      

      For production, install a valid SSL certificate (e.g. Let's Encrypt).

      Getting Help

      If you'reissues still experiencing issues:persist:

      1. Check Documentation: Review this guide thoroughly
      2. Server Logs: Check PatchMon server logs for detailed error detailsinformation
      3. CommunityUse Support:the Ask built-in PatchMonSwagger communityUI forumsto test endpoints interactively
      4. GitHub Issues: Search or create an issue at github.com/9technologygroup/patchmon.netPatchMon/PatchMon
      5. Support:Join Contactthe PatchMon supportcommunity with:on
        • Error message (exact text)
        • Steps to reproduce
        • cURL command that fails (sanitize credentials!)
        • Expected vs actual behavior
        Discord

      API Reference

      Base URL

      https://your-patchmon-instance.com/api/v1
      

      API Version

      Current version: v1

      The API version can be configured via the API_VERSION environment variable (default: v1).

      Authentication

      All API endpoints require Basic Authentication:

      Authorization: Basic <base64(token_key:token_secret)>
      

      Content Type

      All requests and responses use JSON:

      Content-Type: application/json
      

      Rate Limiting

      API requests are subject to rate limiting:

      • Limit: Varies by instance configuration
      • Headers: Rate limit information is included in response headers
      • Exceeded: Returns 429 Too Many Requests

      Endpoints Summary

      EndpointMethodScope RequiredDescription
      /api/v1/api/hostsGEThost:getList hosts with groups

      Changelog

      Version 1.0 (Current)

      • Initial release of API credentials system
      • Basic Authentication with scoped permissions
      • Host listing endpoint with group filtering
      • IP restrictions and expiration dates
      • Integration with auto-enrollment token system

      Appendix

      Scope Reference Table

      ResourceActionHTTP MethodDescription
      hostgetGETRead host information
      hostputPUTReplace entire host object
      hostpatchPATCHPartially update host
      hostupdatePOST/PATCHGeneral update operations
      hostdeleteDELETERemove host from system

      HTTP Status Codes

      CodeMeaningWhen It Occurs
      200OKRequest successful
      400Bad RequestInvalid query parameters
      401UnauthorizedAuthentication failed
      403ForbiddenAuthorization failed
      404Not FoundEndpoint doesn't exist
      429Too Many RequestsRate limit exceeded
      500Internal Server ErrorServer error
      503Service UnavailableServer maintenance

      Glossary

      • API Credential: Authentication token for programmatic access
      • Basic Authentication: HTTP authentication scheme using username:password
      • Scope: Permission defining what actions a credential can perform
      • Token Key: The "username" part of the API credential
      • Token Secret: The "password" part of the API credential (hashed)
      • Host: A server or system monitored by PatchMon
      • Host Group: Collection of hosts for organizational purposes
      • Integration Type: Category of API credential (api, gethomepage, proxmox-lxc)

      Last Updated: November 10, 2025
      API Version: v1
      Document Version: 1.0

      For the latest documentation, visit: https://docs.patchmon.net