This document describes the external REST API introduced in version 4.1.0. External applications authenticate with API keys (Bearer tokens) generated in the web UI. The API is served by the web container — the internal host agent is never exposed to external callers.
- Authentication
- API Key Management (Web UI)
- Scopes
- Base URL & Versioning
- Request & Response Format
- Error Responses
- Quick Start
- Endpoints – Jobs
- Endpoints – Tags
- Endpoints – Export
- Endpoints – Maintenance Windows
- Endpoints – Settings
- Endpoints – Timeline
- Endpoints – Agents
- Endpoints – Audit Log
- Rate Limiting
- What the API does NOT cover
- Troubleshooting
- Changelog
Every API request must carry an Authorization header with the Bearer token:
Authorization: Bearer cm_<your-api-key>Keys are generated in the web UI under API Keys (available to every user). The plain-text key is shown exactly once after creation — store it securely. Only the SHA-256 hash of the key is persisted in the database.
cm_<40 random URL-safe base64 characters>
Example:
cm_aB3xQ7rLmP2kZw9sYvUd1nEjTfHgCo6R8iN0pA4e
| Condition | Error message |
|---|---|
Authorization header missing |
"Missing API key" |
| Key not found in database | "Invalid API key" |
Key has expired (expires_at in the past) |
"API key expired" |
| Caller IP not in IP whitelist | "IP address not allowed" |
| Required scope not granted | "Insufficient scope" (403) |
Keys are managed at /api-keys in the web UI.
| Route | Method | Description |
|---|---|---|
/api-keys |
GET | List all own API keys |
/api-keys/create |
GET | Show creation form |
/api-keys |
POST | Create a new key |
/api-keys/{id}/delete |
POST | Delete a key |
| Field | Required | Description |
|---|---|---|
name |
yes | Human-readable label (e.g. "Grafana Monitor") |
scopes[] |
yes | One or more scope checkboxes (see §3) |
expires_at |
no | ISO-8601 date; empty = no expiry |
agent_ids[] |
no | Restrict to specific agents; empty = all agents |
ip_whitelist |
no | Comma-separated CIDR blocks, e.g. 10.0.0.0/8, 192.168.1.5/32 |
A user can only grant scopes they are allowed to use themselves:
| User role | Grantable scopes |
|---|---|
view |
jobs:read, maintenance:read, export:read, settings:read |
admin |
all 8 scopes |
| Scope | What it grants |
|---|---|
jobs:read |
List/view jobs, tags, execution history, monitor, timeline, swimlane |
jobs:write |
Create, edit, delete jobs; bulk activate/deactivate/delete/re-tag |
jobs:execute |
Trigger "Run Now"; kill a running execution |
export:read |
Download crontab as JSON, CSV, or cron format |
maintenance:read |
List maintenance windows; conflict check |
maintenance:write |
Create, edit, delete maintenance windows |
settings:read |
Read agent settings (mail, Telegram, InfluxDB, notifications) |
settings:write |
Update agent settings; resync crontab; cleanup operations |
audit:read |
Read the audit log — who changed what and when (admin-only scope) |
| Profile | Included scopes |
|---|---|
read-only |
jobs:read, maintenance:read, export:read |
operator |
jobs:read, jobs:execute, maintenance:read |
developer |
jobs:read, jobs:write, jobs:execute, export:read |
full-admin |
all 9 scopes |
All API endpoints are prefixed with /api/v1/.
https://<your-cronmanager-host>/api/v1/
The version segment (v1) will be incremented for breaking changes.
If a key is restricted to specific agents (agent_ids), requests that would
target a different agent return:
{ "error": "Agent not permitted", "code": 403 }The current agent is determined by the X-Agent-Id request header (optional).
If omitted, the default agent is used.
- All request bodies must be
application/json. - All responses are
application/jsonwithcharset=utf-8. - Timestamps are ISO-8601 strings in UTC:
"2026-06-22T14:30:00Z". - Boolean values are JSON booleans (
true/false), not0/1.
Endpoints that return lists support optional query parameters:
| Parameter | Default | Description |
|---|---|---|
limit |
100 |
Maximum number of items (max: 500) |
offset |
0 |
Skip this many items |
Paginated response envelope:
{
"data": [ ... ],
"count": 42,
"limit": 100,
"offset": 0
}All errors follow the same envelope:
{
"error": "Short error type",
"message": "Human-readable explanation.",
"code": 422
}Validation errors include a fields object:
{
"error": "Validation failed",
"fields": {
"schedule": "Invalid cron expression.",
"command": "Command must not be empty."
},
"code": 422
}| HTTP status | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 400 | Bad request (malformed JSON, invalid parameter) |
| 401 | Unauthenticated (missing/invalid/expired key) |
| 403 | Forbidden (insufficient scope or agent restriction) |
| 404 | Resource not found |
| 409 | Conflict (e.g. duplicate) |
| 422 | Validation error |
| 500 | Internal server error |
| 502 | Agent unreachable (web container cannot reach host agent) |
This section shows the most common API calls end-to-end.
Open API Keys in the sidebar, click Create new key, choose a name and the scopes
you need, then click Save. Copy the displayed cm_… token — it will not be shown again.
export CM_URL=https://cronmanager.example.com
export CM_KEY=cm_aB3xQ7rLmP2kZw9sYvUd1nEjTfHgCo6R8iN0pA4eList all active jobs:
curl -s -H "Authorization: Bearer $CM_KEY" \
"$CM_URL/api/v1/jobs?active=1" | jq '.data[] | {id, description, schedule}'Create a new job:
curl -s -X POST \
-H "Authorization: Bearer $CM_KEY" \
-H "Content-Type: application/json" \
-d '{
"linux_user": "deploy",
"schedule": "0 3 * * *",
"command": "/opt/scripts/backup.sh",
"description": "Nightly backup",
"active": true,
"targets": ["local"]
}' \
"$CM_URL/api/v1/jobs"Trigger a job immediately:
curl -s -X POST \
-H "Authorization: Bearer $CM_KEY" \
"$CM_URL/api/v1/jobs/42/execute"Export all jobs as JSON:
curl -s -H "Authorization: Bearer $CM_KEY" \
"$CM_URL/api/v1/export?format=json" | jq .Target a specific agent (multi-agent setup):
curl -s \
-H "Authorization: Bearer $CM_KEY" \
-H "X-Agent-Id: 2" \
"$CM_URL/api/v1/jobs"Required scope for read operations: jobs:read
Required scope for write operations: jobs:write
Required scope for execute operations: jobs:execute
List all cron jobs.
Query parameters:
| Parameter | Description |
|---|---|
tag |
Filter by tag name |
user |
Filter by Linux user |
target |
Filter by execution target |
active |
1 = active only, 0 = inactive only |
limit |
Page size (default: 100, max: 500) |
offset |
Pagination offset |
Response 200:
{
"data": [
{
"id": 1,
"linux_user": "deploy",
"schedule": "*/5 * * * *",
"command": "/opt/scripts/backup.sh",
"description": "Backup database",
"active": true,
"notify_on_failure": true,
"notify_on_recovery": false,
"notify_on_silence": false,
"silence_grace_minutes": null,
"last_silence_alert_at": null,
"execution_limit_seconds": 300,
"auto_kill_on_limit": false,
"singleton": false,
"run_in_maintenance": false,
"retention_days": 30,
"retry_count": 0,
"retry_delay_minutes": 5,
"restart_on_exitcodes": [],
"notify_after_failures": 3,
"notify_after_limit_exceeded": false,
"execution_mode": "local",
"ssh_host": null,
"targets": ["local"],
"tags": ["backup", "daily"],
"created_at": "2026-01-15T08:00:00Z"
}
],
"count": 1,
"limit": 100,
"offset": 0
}Get a single cron job by ID.
Response 200: Single job object (same structure as in the list, without envelope).
Response 404:
{ "error": "Not Found", "message": "Cron job with ID 99 does not exist.", "code": 404 }Create a new cron job. Scope: jobs:write
Request body:
{
"linux_user": "deploy",
"schedule": "0 3 * * *",
"command": "/opt/scripts/backup.sh",
"description": "Nightly backup",
"active": true,
"targets": ["local"],
"tags": ["backup"],
"notify_on_failure": true,
"notify_on_recovery": false,
"notify_on_silence": false,
"silence_grace_minutes": null,
"execution_limit_seconds": 0,
"auto_kill_on_limit": false,
"singleton": false,
"run_in_maintenance": false,
"retention_days": 30,
"retry_count": 0,
"retry_delay_minutes": 5,
"restart_on_exitcodes": [],
"notify_after_failures": 3,
"notify_after_limit_exceeded": false
}Required fields: linux_user, schedule, command, targets (non-empty array).
Response 201: Created job object.
Update a cron job. Scope: jobs:write
Request body: same structure as POST (all fields optional except those noted). Only fields present in the body are updated.
Response 200: Updated job object.
Delete a cron job and remove it from the crontab. Scope: jobs:write
Response 200:
{ "success": true }Trigger an immediate one-time execution of the job. Scope: jobs:execute
Response 200:
{ "success": true, "message": "Job queued for immediate execution." }Kill a running execution by its execution log ID. Scope: jobs:execute
Response 200:
{ "success": true }Execution history for a specific job.
Query parameters:
| Parameter | Description |
|---|---|
limit |
Page size (default: 50) |
offset |
Pagination offset |
status |
Filter: success, failed, running |
Response 200:
{
"data": [
{
"execution_id": 101,
"job_id": 1,
"linux_user": "deploy",
"description": "Backup database",
"schedule": "*/5 * * * *",
"tags": ["backup"],
"started_at": "2026-06-22T03:00:01Z",
"finished_at": "2026-06-22T03:00:47Z",
"exit_code": 0,
"output": "Backup completed: 1.2 GB",
"target": "local",
"during_maintenance": false,
"retry_attempt": 0,
"retry_root_execution_id": null,
"duration_seconds": 46
}
],
"count": 1,
"limit": 50,
"offset": 0
}Required scope: jobs:read
List all tags.
Response 200:
{
"data": [
{ "id": 1, "name": "backup" },
{ "id": 2, "name": "monitoring" }
],
"count": 2
}Required scope: export:read
Export all cron jobs in the requested format.
Query parameters:
| Parameter | Values | Default |
|---|---|---|
format |
json, csv, cron |
json |
Query parameters (all optional):
| Parameter | Description |
|---|---|
format |
json (default), csv, cron |
user |
Export only jobs for this Linux user |
tag |
Export only jobs carrying this tag |
Response 200 (format=json):
{
"export": {
"generated_at": "2026-06-22T14:00:00Z",
"user_filter": null,
"tag_filter": null,
"job_count": 6
},
"data": [ ... ],
"count": 6
}Response 200 (format=csv):
Content-Type: text/csv — CSV download with header row.
Columns: id, linux_user, schedule, command, description, active, tags (pipe-separated), targets (pipe-separated), notify_on_failure, execution_limit_seconds, retry_count, retry_delay_minutes, singleton, run_in_maintenance, retention_days, created_at.
Response 200 (format=cron):
Content-Type: text/plain — ready-to-import crontab lines, grouped by Linux user.
SSH-target jobs are wrapped as ssh -o BatchMode=yes <host> '<command>'.
Required scope for read: maintenance:read
Required scope for write: maintenance:write
List all maintenance windows.
Response 200:
{
"data": [
{
"id": 1,
"target": "host1",
"cron_schedule": "0 2 * * 0",
"duration_minutes": 60,
"description": "Weekly Sunday maintenance",
"active": true,
"created_at": "2026-03-01T00:00:00Z"
}
],
"count": 1
}Get a single maintenance window.
Response 200: Single window object.
Create a maintenance window. Scope: maintenance:write
Request body:
{
"target": "host1",
"cron_schedule": "0 2 * * 0",
"duration_minutes": 60,
"description": "Weekly Sunday maintenance",
"active": true
}Required fields: target, cron_schedule, duration_minutes.
Response 201: Created window object.
Update a maintenance window. Scope: maintenance:write
Response 200: Updated window object.
Delete a maintenance window. Scope: maintenance:write
Response 200:
{ "success": true }Required scope for read: settings:read
Required scope for write: settings:write
Settings are grouped into sections: mail, telegram, influxdb, notifications.
Read all agent settings.
Response 200:
{
"mail": {
"enabled": true,
"from": "cronmanager@example.com",
"to": "admin@example.com",
"host": "smtp.example.com",
"port": 587,
"username": "cronmanager@example.com",
"encryption": "tls"
},
"telegram": {
"enabled": false,
"bot_token": "",
"chat_id": ""
},
"influxdb": {
"enabled": false,
"url": "",
"token": "",
"org": "",
"bucket": ""
},
"notifications": {
"web_url": "https://cronmanager.example.com"
}
}Read a single settings section (mail, telegram, influxdb, notifications).
Response 200: Section object.
Update a settings section. Scope: settings:write
Request body: Partial or full section object (only provided keys are updated).
Response 200: Updated section object.
Resync crontab from database. Scope: settings:write
Response 200:
{ "success": true, "message": "Crontab resynced." }Required scope: jobs:read
Execution history across all jobs.
Query parameters:
| Parameter | Description |
|---|---|
limit |
Page size (default: 100) |
offset |
Pagination offset |
status |
Filter: success, failed, running |
tag |
Filter by job tag |
Response 200:
{
"data": [
{
"execution_id": 101,
"job_id": 1,
"description": "Nightly backup",
"linux_user": "deploy",
"schedule": "0 3 * * *",
"tags": ["backup"],
"started_at": "2026-06-22T03:00:01Z",
"finished_at": "2026-06-22T03:00:47Z",
"exit_code": 0,
"target": "local",
"during_maintenance": false,
"retry_attempt": 0,
"retry_root_execution_id": null,
"duration_seconds": 46
}
],
"count": 1,
"limit": 100,
"offset": 0
}Required scope: settings:read
If the API key has an agent_ids restriction, only those agents are returned.
Sensitive connection fields (url, hmac_secret, ssl_ca_bundle) are never included
in API responses.
List all agents visible to this API key.
Response 200:
{
"data": [
{
"id": 1,
"name": "Default",
"description": "Lokaler Agent",
"enabled": true
},
{
"id": 2,
"name": "Remote-Server",
"description": "Agent auf server2.example.com",
"enabled": true
}
],
"count": 2
}Response 401 / 403: See §6 Error Responses.
Required scope: audit:read
This scope can only be granted by admin users. It provides read-only access to the audit log, which records all create, update, and delete operations performed in the UI or via the API — including before/after diffs for updates and snapshots for creates and deletes.
Return a paginated list of audit log entries with optional filters.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 100 |
Max entries per page (max: 500) |
offset |
int | 0 |
Skip this many entries |
username |
string | — | Filter by exact username |
action_prefix |
string | — | Filter by action prefix (e.g. cron matches cron.create, cron.update, …) |
date_from |
string | — | Lower bound on created_at (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS) |
date_to |
string | — | Upper bound on created_at |
Response 200:
{
"data": [
{
"id": 42,
"user_id": 1,
"username": "admin",
"action": "cron.update",
"resource_type": "cron",
"resource_id": 17,
"resource_label": "Nightly backup",
"details": { "schedule": "0 2 * * * → 0 3 * * *" },
"ip_address": "10.0.0.5",
"created_at": "2026-06-24 14:30:00"
}
],
"total": 1,
"limit": 100,
"offset": 0
}Known action names:
| Action | Trigger |
|---|---|
cron.create |
New job created (snapshot of initial settings) |
cron.update |
Job settings changed (only the changed fields, old → new) |
cron.delete |
Job deleted (snapshot of settings at deletion time) |
cron.bulk_status |
Bulk activate/deactivate |
cron.bulk_delete |
Bulk delete |
cron.bulk_tag |
Bulk re-tag |
cron.execute_now |
"Run Now" triggered |
cron.kill |
Running execution killed |
maintenance_window.create |
Maintenance window created (snapshot) |
maintenance_window.update |
Maintenance window settings changed (diff) |
maintenance_window.delete |
Maintenance window deleted (snapshot) |
tag.create |
Tag created |
tag.delete |
Tag deleted |
settings.update |
Agent settings changed (section names only; no credentials logged) |
user.update_role |
User role changed (from/to in details) |
user.delete |
User account deleted |
Response 401 / 403: See §6 Error Responses.
The external API currently applies no rate limiting at the application level. In production
deployments it is strongly recommended to configure rate limiting at the reverse proxy or
load-balancer tier (e.g. nginx limit_req, Caddy rate-limit middleware, or a WAF) to protect
against brute-force token guessing and unintended denial-of-service from misconfigured scripts.
The following operations are intentionally only available through the web UI and cannot be performed via the REST API:
| Operation | Reason |
|---|---|
| Create / edit / delete users | User management is always session-based; admin intent must be explicit |
| Add / edit / delete agents | Agent configuration changes affect the entire installation |
| Manage API keys | An API key cannot create or revoke other API keys |
| Change login credentials | Password changes require the current password (security policy) |
| OIDC / SSO configuration | Sensitive IdP credentials are never exposed over the API |
The Authorization header is missing or malformed. Check that you are sending:
Authorization: Bearer cm_<your-key>Note: the Bearer prefix (including the space) is required.
The token does not match any stored hash. Possible causes:
- The key was deleted in the UI.
- A typo or truncation in the token value.
- The key was issued for a different Cronmanager instance.
The key's expires_at date has passed. Create a new key in the web UI.
Your client IP is not in the key's IP whitelist. Either update the whitelist in the UI or
add the calling machine's IP in CIDR notation (e.g. 192.168.1.10/32).
The key does not have the scope required by the endpoint. Delete the key and create a new one with the appropriate scope (scopes cannot be edited after creation).
The web container cannot reach the host agent. Check:
- The agent container / service is running (
docker psorsystemctl status cronmanager-agent). - The agent URL in the web config (
/var/www/conf/config.json) is correct. - No firewall rule blocks the internal Docker network traffic.
POST /api/v1/jobs/{id}/execute queues an immediate one-time cron entry. The entry is
executed by the cron daemon inside the agent container, not in real time. There may be up
to 60 seconds of delay before the daemon picks up the entry.
| Version | Change |
|---|---|
| 4.5.0 | Added notify_on_silence (bool), silence_grace_minutes (int|null), last_silence_alert_at (string|null, read-only) to job objects; GET /health extended with silent_jobs (int|null) and last_execution_at (string|null) |
| 4.3.4 | Added GET /api/v1/audit endpoint (audit:read scope, admin-only); added §15 Audit Log |
| 4.2.0 | Added GET /api/v1/agents endpoint (settings:read scope; respects agent_ids restriction; omits sensitive fields) |
| 4.1.0 | Initial external REST API with API key authentication and scope-based authorization |