Integration API documentation
Table of Contents
- Overview
- Creating API Credentials
- Authentication
- Available Scopes & Permissions
- API Endpoints
- Usage Examples
- Expected Outputs
- Security Best Practices
- Troubleshooting
- API Reference
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
- Log in to your PatchMon instance
- Go to Settings → Integrations
- 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
- Review your configuration
- Click "Create Token"
- 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:
- Copy both the Token Key and Token Secret
- Store them securely (password manager, secrets vault, etc.)
- Test the credential immediately
- 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 IP10.0.0.0/24- CIDR range192.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
- Combine your token key and secret with a colon:
token_key:token_secret - Encode the string in Base64
- Prepend "Basic " to the encoded string
- Send in the
Authorizationheader
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):
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
{
"error": "Invalid API key"
}
{
"error": "API key is disabled"
}
{
"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:
- Immediately disable the credential in PatchMon UI
- Review "Last Used" timestamp to understand exposure
- Check server logs for unauthorized access
- Create new credentials with different scope if needed
- Delete compromised credentials after verification
- Notify security team if data was accessed
- 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:
- Verify the token key is correct (check for copy/paste errors)
- Confirm the credential exists in PatchMon UI
- Check for extra whitespace or special characters
- Try creating a new credential and testing
Problem: "Invalid API secret"
Possible Causes:
- Token secret is incorrect
- Secret was copied incorrectly
Solution:
- Secrets cannot be retrieved - create a new credential
- Ensure no trailing spaces or newlines in secret
- Verify the entire secret was copied
Problem: "API key is disabled"
Possible Causes:
- Credential was manually disabled
- Security incident response
Solution:
- Check credential status in PatchMon UI (Settings → Integrations → API)
- Click "Enable" to reactivate
- Or create a new credential if this one should remain disabled
Problem: "API key has expired"
Possible Causes:
- Expiration date has passed
Solution:
- Create a new credential to replace the expired one
- Update your systems with the new credentials
- Delete the old credential
Permission Issues
Problem: "Access denied"
Possible Causes:
- Credential doesn't have required scope
- Wrong integration type
Solution:
- Check the error message for specific permission needed
- Go to Settings → Integrations → API
- Click "Edit" on the credential
- Add the required scope (e.g.,
host: get) - 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:
- Identify your actual source IP:
curl https://ifconfig.me - Add this IP to the credential's allowed IP ranges
- If behind a proxy/NAT, add the proxy's IP
- Consider removing IP restrictions for development
Problem: Connection timeout
Possible Causes:
- Network connectivity issues
- Firewall blocking access
- PatchMon server is down
Solution:
- Test basic connectivity:
ping patchmon.example.com curl -I https://patchmon.example.com/health - Check firewall rules
- Verify PatchMon server status
- Try from a different network
Problem: SSL certificate errors
Possible Causes:
- Self-signed certificate
- Expired certificate
- Certificate name mismatch
Solution:
- For production: Fix the SSL certificate
- For development only:
curl -k ... # Skip verification (insecure) - 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:
- Verify hosts exist: Check PatchMon UI → Hosts
- Check filter spelling:
# Wrong ?hostgroup=Productin # Correct ?hostgroup=Production - 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:
- Verify endpoint URL is correct
- Check API version in URL (
/api/v1/...) - Add verbose output to see full response:
curl -v ... - 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:
- Check Documentation: Review this guide thoroughly
- Server Logs: Check PatchMon server logs for error details
- Community Support: Ask in PatchMon community forums
- GitHub Issues: Search or create issue at github.com/9technologygroup/patchmon.net
- 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
No comments to display
No comments to display