Management API
This content is not available in your language yet.
Create, read and update monitors from scripts, CI pipelines and infrastructure-as-code tooling. The API speaks the same manifest format you can export and import in the panel, so a manifest downloaded from the panel works as an API payload and vice versa.
Base URL and authentication
https://app.cronitorex.com/api/v1Authorization: Bearer <api_key>The Management API uses its own key (prefix mk_), separate from the Ingest API key used for pings. Generate it in the panel under Settings → API key (Management API Key section). All requests and responses are application/json. Error messages are always in English.
Rate limit
60 requests per minute per API key. Exceeding the limit returns 429 Too Many Requests. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining headers.
The manifest format
Every monitor is described by a manifest with manifest_version: 1 and one of three kinds:
| Kind | What it monitors |
|---|---|
ping | A cron job or scheduled task that sends pings |
http_check | An HTTP endpoint checked on a schedule |
ssl_check | An SSL certificate checked for expiry |
Ping monitor
{ "manifest_version": 1, "kind": "ping", "name": "db-backup", "enabled": true, "timeout_seconds": 60, "expected_interval_seconds": 86400, "grace_seconds": 300, "tags": ["prod", "backup"]}The name is the slug you ping (a-zA-Z0-9_-, max 64 chars) and it is immutable after creation. expected_interval_seconds and grace_seconds are optional.
HTTP check
{ "manifest_version": 1, "kind": "http_check", "name": "Website health", "enabled": true, "schedule": "*/5 * * * *", "tags": ["prod"], "config": { "url": "https://example.com/health", "method": "GET", "timeout": 30, "expected_status": 200, "expected_content": "" }}schedule accepts either a standard 5-field cron expression (*/5 * * * *) or an interval shorthand (30s, 5m, 1h, 1d, 7d). At least one of expected_status or expected_content must be set.
SSL check
{ "manifest_version": 1, "kind": "ssl_check", "name": "example.com certificate", "enabled": true, "schedule": "0 6 * * *", "config": { "domain": "example.com", "warning_days": 30, "alert_days": 7 }}Endpoints
API responses add a read-only uuid field to the manifest. A uuid sent in a request body is ignored.
List monitors
curl -H "Authorization: Bearer $API_KEY" \ https://app.cronitorex.com/api/v1/monitorsOptional filter: ?kind=ping, ?kind=http_check or ?kind=ssl_check.
{ "monitors": [ { "uuid": "3b0043f4-...", "manifest_version": 1, "kind": "ping", "name": "db-backup", ... } ]}Get a monitor
curl -H "Authorization: Bearer $API_KEY" \ https://app.cronitorex.com/api/v1/monitors/<uuid>Returns the manifest with uuid. Unknown uuid returns 404.
Create a monitor
curl -X POST \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"manifest_version":1,"kind":"ping","name":"db-backup","timeout_seconds":60}' \ https://app.cronitorex.com/api/v1/monitorsReturns 201 Created with the stored manifest including its uuid. Duplicate names and plan limits are rejected with 422.
Update a monitor
curl -X PUT \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"manifest_version":1,"kind":"ping","name":"db-backup","timeout_seconds":120}' \ https://app.cronitorex.com/api/v1/monitors/<uuid>Send the full manifest, not a partial patch. The kind must match the existing monitor and a ping monitor’s name cannot change; both violations return 422.
Delete a monitor
Deletion is irreversible: removing a ping monitor also removes its event history, expected executions and related notifications. A bare DELETE is therefore refused with 403 and a link to the panel, so a mistyped script or a generic CRUD client cannot wipe data by accident:
{ "message": "Deleting a monitor is irreversible. Repeat the request with ?confirm=true, or delete it from the web panel.", "panel_url": "https://app.cronitorex.com/monitors/<uuid>/edit"}Add ?confirm=true to actually delete. The response is 204 No Content with an empty body:
curl -sS -X DELETE \ -H "Authorization: Bearer $CRONITOREX_MANAGEMENT_KEY" \ "https://app.cronitorex.com/api/v1/monitors/<uuid>?confirm=true"Works for all three kinds. For http_check and ssl_check only the check and its execution history go away; for ping monitors the monitor name is released, so a monitor re-created later with the same name starts with a clean history.
Config as code
Instead of driving the endpoints above one monitor at a time, you can keep your whole monitoring setup in a single file, commit it to your repository, and reconcile the account with it in one request.
A bundle is the manifest format wrapped in an array:
{ "manifest_version": 1, "monitors": [ { "manifest_version": 1, "kind": "ping", "name": "db-backup", "timeout_seconds": 60 }, { "manifest_version": 1, "kind": "http_check", "name": "Website health", "schedule": "5m", "config": { "url": "https://example.com/health", "expected_status": 200 } } ]}Monitors are matched by kind and name, not by uuid, so a bundle is portable between accounts and readable in a pull request. Ping monitors and checks have separate name spaces: a ping named api and an http_check named api are two different monitors and neither collides with the other.
Export the account as a bundle
curl -H "Authorization: Bearer $CRONITOREX_MANAGEMENT_KEY" \ https://app.cronitorex.com/api/v1/monitors/export > monitors.jsonThe output is a ready-to-apply bundle with no uuid fields. This is the fastest way to adopt config as code on an account that was set up through the panel: export once, commit the file, and manage it from there.
Apply a bundle
curl -X POST \ -H "Authorization: Bearer $CRONITOREX_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ --data-binary @monitors.json \ "https://app.cronitorex.com/api/v1/monitors/apply?dry_run=true"dry_run=true reports what would change and writes nothing. Drop it to actually apply. Both modes return 200 and the same report:
{ "dry_run": true, "summary": { "create": 1, "update": 1, "unchanged": 5, "orphaned": 2 }, "changes": [ { "name": "db-backup", "kind": "ping", "action": "update", "uuid": "3b0043f4-...", "diff": { "grace_seconds": [300, 600] } }, { "name": "etl-job", "kind": "ping", "action": "create" } ], "orphaned": [ { "name": "old-cleanup-job", "kind": "ping", "uuid": "9c1f77a2-..." } ]}Three properties worth relying on:
- Apply never deletes. Monitors that exist on the account but are missing from the bundle are listed under
orphanedand left untouched. Removing a monitor stays a deliberate act:DELETE ?confirm=true, or the panel. - All or nothing. The whole bundle is validated first. If any entry is invalid the request returns
422and nothing is written, so a typo in the tenth monitor cannot leave the first nine half-applied. - Idempotent. Applying an unchanged bundle reports everything as
unchangedand performs no writes, which makes it safe to run on every push.
Errors are reported per position in the bundle:
{ "message": "The given data was invalid.", "errors": { "monitors.3": ["The \"schedule\" field must be a cron expression (5 fields) or an interval such as 5m, 1h, 1d."] }}Example: reconcile monitors from CI
Dry-run on pull requests so reviewers see the diff, apply on merge:
- name: Check monitor changes if: github.event_name == 'pull_request' run: | curl -sS --fail-with-body -X POST \ -H "Authorization: Bearer ${{ secrets.CRONITOREX_MANAGEMENT_KEY }}" \ -H "Content-Type: application/json" \ --data-binary @monitors.json \ "https://app.cronitorex.com/api/v1/monitors/apply?dry_run=true"
- name: Apply monitors if: github.ref == 'refs/heads/main' run: | curl -sS --fail-with-body -X POST \ -H "Authorization: Bearer ${{ secrets.CRONITOREX_MANAGEMENT_KEY }}" \ -H "Content-Type: application/json" \ --data-binary @monitors.json \ https://app.cronitorex.com/api/v1/monitors/applyUsing the shell client instead of curl
From v1.3 the client wraps both endpoints:
cronitorex export -o monitors.json # download the account as a bundlecronitorex apply -f monitors.json --dry-runcronitorex apply -f monitors.jsonThe client reads the management key from CRONITOREX_MANAGEMENT_KEY, falling back to ~/.cronitorex-management.conf (written by cronitorex configure --management-key, permissions 600). That file is deliberately separate from ~/.cronitorex.conf: the ingest key belongs on every monitored server and is often distributed by configuration management, while the management key can delete monitors together with their event history. Keeping them apart means shipping your ingest config around never fans out a destructive credential.
Passing the wrong kind of key is caught before the request goes out, so you get an explanation rather than a bare 401. Exit status is 0 for a successful apply and 1 for any error, which is what CI needs.
Config as code is available on every plan by default. If it has been disabled for your plan, apply returns 403 with a link to pricing; export keeps working regardless.
Error responses
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
404 | Monitor not found (or belongs to another account) |
422 | Invalid manifest, duplicate name, immutable field or plan limit reached |
403 | DELETE without ?confirm=true, or apply on a plan without config as code |
429 | Rate limit exceeded |
Validation errors use this shape:
{ "message": "A Ping monitor name is immutable. To use a different name, import the manifest as a new monitor.", "errors": { "manifest": ["A Ping monitor name is immutable. To use a different name, import the manifest as a new monitor."] }, "error_codes": { "manifest": ["name_immutable"] }}Machine-readable error codes
errors is meant for humans and its wording changes over time. Anything programmatic should read error_codes, which mirrors errors key for key and position for position with stable identifiers. Errors with a single cause (the 403s above, for example) carry a flat code field instead.
For bundles the keys carry the position, matching Laravel’s array validation format:
{ "message": "The given data was invalid.", "errors": { "monitors.3": ["The \"schedule\" field must be a cron expression (5 fields) or an interval such as 5m, 1h, 1d."] }, "error_codes": { "monitors.3": ["schedule_invalid"] }}Current codes: manifest_version_unsupported, invalid_json, kind_invalid, name_invalid, name_charset_invalid, timeout_out_of_range, interval_out_of_range, grace_out_of_range, schedule_invalid, domain_invalid, url_invalid, method_invalid, check_timeout_invalid, http_assertion_required, kind_mismatch, name_immutable, name_already_exists, name_taken_by_other_kind, plan_limit_reached, bundle_invalid, monitors_missing, entry_not_object, duplicate_entry, delete_requires_confirm, config_as_code_disabled.
New codes get added over time and existing ones are never renamed or repurposed, so treat an unrecognised value as a generic failure rather than rejecting the response.
Machine-readable API description
The full OpenAPI 3 description lives at docs.cronitorex.com/openapi.yaml and covers both the Ingest and Management APIs. Point a generator at it to get a typed client in your language of choice. A Postman collection is available too.
Example: create a monitor from CI
A GitHub Actions step that registers a monitor after every deploy (idempotent: a 422 duplicate response means the monitor already exists):
- name: Ensure deploy monitor exists run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ -H "Authorization: Bearer ${{ secrets.CRONITOREX_API_KEY }}" \ -H "Content-Type: application/json" \ -d '{"manifest_version":1,"kind":"ping","name":"nightly-report","expected_interval_seconds":86400}' \ https://app.cronitorex.com/api/v1/monitors) if [ "$STATUS" != "201" ] && [ "$STATUS" != "422" ]; then echo "Unexpected status $STATUS"; exit 1 fi