curl:
Endpoint reference
Management endpoints are API-key authenticated withAuthorization: Bearer mantis_live_....
Dashboard helper endpoints that need browser access also accept the mantis_session
httpOnly cookie. Public trigger, status, health, wallet, and dev-inbox routes
are called out explicitly below.
Parsed request bodies are bounded.
POST /api/keys and
POST /api/api-keys reject JSON bodies over 64 KiB with
413 payload_too_large. Apple Wallet log bodies are capped at 32 KiB, and the
dev inbox capture path is capped at 1 MiB.
Supported installer type values are shell, shell-sudo,
macos-login, macos-boot, macos-wake, macos-network,
linux-boot, linux-wake, linux-network, windows-logon,
windows-wake, windows-network, css-background, js-clone-detector,
nfc-ndef, homeassistant, homeassistant-receiver, and scrypted.
Webhook destinations get an HMAC secret. Outbound raw-webhook deliveries include
X-Mantis-Timestamp and X-Mantis-Signature: sha256=<hex> over
<timestamp>.<json body>. The plaintext secret is only shown on create, replace,
explicit reveal, or rotate responses; normal listing returns a fingerprint.
API key scope: full vs enroll
Every API key carries ascope, orthogonal to is_admin:
full(default) — behaves as described throughout this page. Subject to the admin / non-admin visibility rules.enroll— create-only. An enroll key may call onlyPOST /api/keys. Every other management route (list/read/update/delete keys — including the ones it created — plus/api/hits/recent,/api/api-keys, the audit log, and any session-reachable route) returns403 forbidden, and an enroll key cannot log in to the dashboard.is_admin: truetogether withscope: "enroll"is rejected at validation.
- It can recover the trigger URL of any key whose
external_idit guesses. APOST /api/keysthat collides with an existingexternal_idreturns that key’s trigger URL,public_idand expiry ("reused": true, HTTP200) even when a different API key created it — the memo isnullin that case and alert routing is never included; see Idempotent creation. This is what lets a re-imaged machine, or a rotated enroll key, find its canary again, and it is the one thing an extracted enroll key can do beyond creating keys.mantis devicederivesexternal_ids deterministically asmantis:device:<os>:<normalized-name>:<slug>, so an attacker who knows your naming convention can guess a machine’s ids and read back that machine’s canary URLs, which is exactly what lets an intruder route around the tripwires. Each cross-key claim is recorded in the audit log askey.claimedwithcross_key: true— watch for a burst of them. (A full-scope key that did not create the key gets409instead and learns nothing.) - It can supply
destinationson creation, and Mantis fires the activation ping synchronously — so the key can make your instance POST to an attacker-chosen HTTP(S) endpoint (private, loopback and metadata addresses are rejected unlessALLOW_PRIVATE_WEBHOOKS=1) or, ifSMTP_URLis set, send it mail.
deploy/kandji/.
Idempotent creation
POST /api/keys also accepts an optional external_id (1–128 chars, matching
^[A-Za-z0-9][A-Za-z0-9._:-]*$) stored on a unique column. When supplied, a
repeat POST with the same external_id returns the existing key —
"reused": true with HTTP 200 instead of 201 — rather than minting a
duplicate. The other body fields (memo, destinations, …) apply only when the
row is actually created; a later claim never mutates what the key was first
configured with. Keys created without an external_id are unaffected (unique
constraint treats NULLs as distinct).
This is the mechanism the fleet-enrollment flow relies on — one key per machine
serial, so re-running enrollment on a reimaged machine reuses its key instead of
littering the list. What a repeat POST returns depends on who is asking:
- the key’s creator, or an admin — the key as they could read it anyway (full shape for full keys; the reduced shape below for enroll keys);
- an enrollment-scoped key that did not create it — the reduced shape:
trigger URL,
public_id,external_id, expiry anddisabled, withmemoset tonull. No alert routing, no signing secrets. Audited askey.claimedwithcross_key: true; - any other full-scope key —
409 conflictwith no key details, audited askey.claimedwithdenied: true. If you rotate the full-scope key that ran a pre-provisioning script, re-run it with an admin key.
409 conflict; retry.
Notification rows in hit listings
Every hit returned by/api/keys/:id/hits and /api/hits/recent carries a
notifications array — one row per destination the hit fanned out to, with
channel, status, attempts, max_attempts, next_attempt_at,
succeeded_at, last_error, target and destination_scope.
destination_scope is key for a destination attached to the key itself,
global for an instance-wide global destination,
and unknown when the destination has since been deleted. target is null
unless the caller may see it: admins always may; a non-admin key owner sees
only the targets of the key’s own destinations. Global-destination targets are
Slack / Discord / Teams / Home Assistant webhook URLs configured by an admin —
they are credentials, so a non-admin never receives them. The dashboard hit
feed and mantis hits render a placeholder for redacted rows.
Response kinds for the trigger endpoint
Webhook payload
host_context object when the hit came from one of our installer snippets:
shell-sudo hit you’d see "source": "shell-sudo", "sudo_cmd": "apt update --quiet". For a linux-network hit, "source": "linux-network", "network_interface": "wlan0". Fields not relevant to the installer are null.
host_context is null for hits that didn’t include X-Mantis-* headers (e.g., a file/folder key, or a regular curl to the URL).
Webhooks are sent through a Postgres-backed retry queue (no Redis required). On failure the notification is retried with exponential backoff: 1m, 5m, 30m, 2h, 12h (each with ±20% jitter), giving up after 5 attempts. Delivery state is tracked on each hit’s notifications array.