Skip to main content

Integration API documentation

Table of Contents


Overview

PatchMon API Credentials provide programmatic access to your PatchMon instance, enabling automation, integration with third-party tools, and custom workflows. API credentials use 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
  • 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

Creating API Credentials

Step-by-Step Guide

1. Navigate to Integrations Page

  1. Log in to your PatchMon instance
  2. Go to SettingsIntegrations
  3. Click on the API tab

2. Click "New Credential"

Click the "New Credential" button in the top-right corner of the API tab.

3. Configure the Credential

Fill in the following fields in the creation form:

Required Fields

Token Name *

  • A descriptive name for this credential
  • Examples: "Ansible Inventory", "Monitoring Dashboard", "CI/CD Pipeline"
  • Used for identification and audit purposes

Scopes *

  • Select the permissions this credential should have
  • At least one permission must be selected
  • Available scopes are detailed in the Available Scopes & Permissions section
Optional Fields

Allowed IP Addresses

  • Comma-separated list of IP addresses that can use this credential
  • Examples: 192.168.1.100, 10.0.0.50, 10.0.0.51
  • Leave empty to allow access from any IP address
  • Supports single IP addresses or CIDR notation

Expiration Date

  • Set an automatic expiration date for the credential
  • Uses ISO 8601 datetime format (datetime-local input)
  • Leave empty for credentials that don't expire
  • Expired credentials are automatically rejected

4. Review and Create

  1. Review your configuration
  2. Click "Create Token"
  3. The credential will be generated immediately

5. Save Your Credentials

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

After creation, you'll see a modal displaying:

  • Token Name: Your chosen name
  • Token Key: The API key (username)
  • Token Secret: The API secret (password) - only shown once
  • Granted Scopes: The permissions assigned
  • Usage Examples: Pre-filled cURL commands

Important Actions:

  1. Copy both the Token Key and Token Secret
  2. Store them securely (password manager, secrets vault, etc.)
  3. Test 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)

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 string in Base64
  3. Prepend "Basic " to the encoded string
  4. Send in the Authorization header

Most HTTP clients handle this automatically with the -u flag (cURL) or auth parameter (Python requests).

Authentication Flow

┌─────────────┐                                  ┌─────────────┐
│   Client    │                                  │  PatchMon   │
│ Application │                                  │   Server    │
└──────┬──────┘                                  └──────┬──────┘
       │                                                │
       │  1. Send Request with Basic Auth              │
       │  Authorization: Basic <base64>                │
       │──────────────────────────────────────────────>│
       │                                                │
       │                  2. Validate Credentials      │
       │                     - Decode Base64           │
       │                     - Find token in DB        │
       │                     - Verify secret (bcrypt)  │
       │                     - Check if active         │
       │                     - Check expiration        │
       │                     - Verify IP (if set)      │
       │                     - Validate scopes         │
       │                                                │
       │                  3. Return Response           │
       │<──────────────────────────────────────────────│
       │  200 OK + Data (if authorized)                │
       │  401 Unauthorized (if auth fails)             │
       │  403 Forbidden (if scope missing)             │
       │                                                │
       │                  4. Update last_used_at       │
       │                     timestamp in DB           │
       │                                                │

Validation Steps

The server performs the following validation checks (in order):

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

Response Codes

Code Description Reason
200 OK Request successful Authentication and authorization passed
401 Unauthorized Authentication failed Invalid credentials, expired, or inactive
403 Forbidden Authorization failed Valid credentials but insufficient permissions
500 Internal Server Error Server error Unexpected error during authentication

Available Scopes & Permissions

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

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

Current Resources

Host Resource

Resource Name: host

Available Actions:

Action Description HTTP Methods Use Case
get Read host data GET Retrieve host information, query hosts
put Replace host data PUT Replace entire host object
patch Update host data PATCH Partial updates to host
update Modify host data POST, PATCH General update operations
delete Delete hosts DELETE Remove hosts from system

Example Scope Configuration:

{
  "host": ["get"]  // Read-only access to hosts
}
{
  "host": ["get", "patch"]  // Read and update hosts
}
{
  "host": ["get", "put", "patch", "update", "delete"]  // Full access
}

Scope Inheritance

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

For example, get permission does not automatically include patch permission.

Minimum Scopes

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


API Endpoints

List Hosts

Get a list of hosts with their IP addresses and group memberships.

Endpoint

GET /api/v1/api/hosts

Required Scope

{
  "host": ["get"]
}

Query Parameters

Parameter Type Required Description Example
hostgroup string No Filter by host group names or UUIDs (comma-separated) Production,Development or uuid1,uuid2

Request Headers

Authorization: Basic <base64(token_key:token_secret)>
Content-Type: application/json

Response

Success (200 OK):

{
  "hosts": [
    {
      "id": "uuid-1234",
      "friendly_name": "web-server-01",
      "hostname": "web01.example.com",
      "ip": "192.168.1.100",
      "host_groups": [
        {
          "id": "group-uuid-1",
          "name": "Production"
        },
        {
          "id": "group-uuid-2",
          "name": "Web Servers"
        }
      ]
    },
    {
      "id": "uuid-5678",
      "friendly_name": "db-server-01",
      "hostname": "db01.example.com",
      "ip": "192.168.1.101",
      "host_groups": [
        {
          "id": "group-uuid-1",
          "name": "Production"
        },
        {
          "id": "group-uuid-3",
          "name": "Database Servers"
        }
      ]
    }
  ],
  "total": 2,
  "filtered_by_groups": ["Production"]  // Only present if filtering
}

Response 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[].id string (UUID) Group identifier
hosts[].host_groups[].name string Group name
total integer Total number of hosts returned
filtered_by_groups array Groups used for filtering (if applicable)

Filtering by Host Groups

Filter by Group Name:

GET /api/v1/api/hosts?hostgroup=Production

Filter by Multiple Groups (OR logic - hosts in ANY of the 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

⚠️ Important: URL Encoding for Group Names with Spaces

If your host group names contain spaces, you must URL-encode them using %20:

# Group name: "Web Servers"
GET /api/v1/api/hosts?hostgroup=Web%20Servers

# Group name: "Production Database"
GET /api/v1/api/hosts?hostgroup=Production%20Database

# Multiple groups with spaces
GET /api/v1/api/hosts?hostgroup=Web%20Servers,Database%20Servers

Most HTTP clients handle this automatically, but if constructing URLs manually, remember to replace spaces with %20.

Error Responses

401 Unauthorized - Invalid or expired credentials:

{
  "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 address not allowed"
}

403 Forbidden - Missing scope:

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

500 Internal Server Error - Server error:

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

Usage Examples

cURL Examples

Basic Request - List All Hosts

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

Filter by Single Group

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

Filter by Multiple Groups

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

Filter by Group with Spaces in Name

# Group name: "Web Servers" - use %20 for spaces
curl -u "patchmon_api_abc123:def456xyz789" \
  "https://patchmon.example.com/api/v1/api/hosts?hostgroup=Web%20Servers"

Pretty Print JSON Output

curl -u "patchmon_api_abc123:def456xyz789" \
  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"
API_SECRET = "def456xyz789"
BASE_URL = "https://patchmon.example.com"

# Create session with authentication
session = requests.Session()
session.auth = HTTPBasicAuth(API_KEY, API_SECRET)

# Get 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: {host['friendly_name']} ({host['ip']})")
        print(f"  Groups: {', '.join([g['name'] for g in host['host_groups']])}")
else:
    print(f"Error: {response.status_code} - {response.json()}")

Filter by Host Group

# Get hosts in Production 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 spaces (requests handles URL encoding automatically)
response = session.get(
    f"{BASE_URL}/api/v1/api/hosts",
    params={"hostgroup": "Web Servers"}  # Automatically encoded to Web%20Servers
)

Error Handling

def get_hosts(hostgroup=None):
    """Get hosts with error 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"
API_SECRET = "def456xyz789"
BASE_URL = "https://patchmon.example.com"

def generate_ansible_inventory():
    """Generate Ansible inventory from PatchMon hosts"""
    
    # Authenticate
    auth = HTTPBasicAuth(API_KEY, API_SECRET)
    response = requests.get(f"{BASE_URL}/api/v1/api/hosts", auth=auth)
    
    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 axios

const axios = require('axios');

// API credentials
const API_KEY = 'patchmon_api_abc123';
const API_SECRET = 'def456xyz789';
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;
  }
}

// Usage
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:

#!/usr/bin/env python3
"""
PatchMon Dynamic Inventory Script for Ansible
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
        )
        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

# Export credentials
export PATCHMON_API_KEY="patchmon_api_abc123"
export PATCHMON_API_SECRET="def456xyz789"
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"
}

Security Best Practices

Credential Management

1. Store Credentials Securely

✅ DO:

  • Use a password manager (1Password, LastPass, Bitwarden)
  • Store in secrets management systems (HashiCorp Vault, AWS Secrets Manager)
  • Use environment variables for automation
  • Encrypt credentials at rest

❌ DON'T:

  • Hard-code credentials in source code
  • Commit credentials to version control
  • Share credentials via email or chat
  • Store in 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:

Allowed IPs: 192.168.1.100, 10.0.0.50

For dynamic IPs, consider:

  • VPN with static exit IPs
  • Cloud provider NAT gateway IPs
  • Proxy servers with static IPs

Network Security

1. Always Use HTTPS

✅ DO:

curl -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. Verify SSL Certificates

# Default (verifies SSL)
curl -u "key:secret" https://patchmon.example.com/api/v1/api/hosts

# Only disable for testing/development
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. Use Firewall Rules

Restrict PatchMon API access at the network 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
  • Investigate credentials that haven't 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:

  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)
  2. Click "Enable" to reactivate
  3. Or create a new credential if 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 one
  2. Update your systems with the new credentials
  3. Delete the old credential

Permission Issues

Problem: "Access denied"

Possible Causes:

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

Solution:

  1. Check the error message for specific permission needed
  2. Go to Settings → Integrations → API
  3. Click "Edit" on the credential
  4. Add the required scope (e.g., host: get)
  5. Save and retry the request

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.

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 Errors Reference

Error Message HTTP Code Cause Solution
"Missing or invalid authorization header" 401 No Authorization header or wrong format Use -u key:secret with cURL
"Invalid credentials format" 401 Authorization header doesn't contain key:secret Check format is key:secret with colon
"Invalid API key" 401 Token key not found Verify credential exists and key is correct
"API key is disabled" 401 Credential inactive Enable credential or create new one
"API key has expired" 401 Past expiration date Create new credential
"Invalid API key type" 401 Not an API integration type Credential must be type "api"
"Invalid API secret" 401 Secret doesn't match hash Recreate credential
"IP address not allowed" 403 Source IP not in allowed ranges Add IP to allowed ranges
"Access denied" 403 Missing required scope Add required permission to credential
"Failed to fetch hosts" 500 Server error Check server logs, contact support

Debug Mode

Enable verbose output for troubleshooting:

cURL

# Verbose mode - shows full request/response
curl -v -u "key:secret" 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

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

# Make request - will show full details
response = requests.get(url, auth=auth)

JavaScript

const axios = require('axios');

// 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;
});

Getting Help

If you're still experiencing issues:

  1. Check Documentation: Review this guide thoroughly
  2. Server Logs: Check PatchMon server logs for error details
  3. Community Support: Ask in PatchMon community forums
  4. GitHub Issues: Search or create issue at github.com/9technologygroup/patchmon.net
  5. Support: Contact PatchMon support with:
    • Error message (exact text)
    • Steps to reproduce
    • cURL command that fails (sanitize credentials!)
    • Expected vs actual behavior

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

Endpoint Method Scope Required Description
/api/v1/api/hosts GET host:get List 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

Resource Action HTTP Method Description
host get GET Read host information
host put PUT Replace entire host object
host patch PATCH Partially update host
host update POST/PATCH General update operations
host delete DELETE Remove host from system

HTTP Status Codes

Code Meaning When It Occurs
200 OK Request successful
400 Bad Request Invalid query parameters
401 Unauthorized Authentication failed
403 Forbidden Authorization failed
404 Not Found Endpoint doesn't exist
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server error
503 Service Unavailable Server 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