Meridian exposes a bearer-token authenticated REST API at
/api/v1/. Every UI action in the portal is a thin wrapper
over this API — anything you can do with a browser session, you can
automate with an API token. The full machine-readable spec is served
at /openapi.json (Swagger/OpenAPI 3.1),
and an interactive browser is available at /docs.
In the portal: Settings → API tokens → Create token. The plaintext token is shown once; store it immediately. Its SHA-256 hash is persisted server-side — Meridian cannot recover the original. Tokens carry:
Send the token in the Authorization header with a
Bearer prefix. Meridian recognises tokens by the
mrd_ prefix; anything else on that header is treated as a
session ID (legacy).
curl -sS https://meridian.example.com/api/v1/monitors/ \
-H "Authorization: Bearer mrd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
For POST/PATCH/PUT/DELETE on routes that normally require a CSRF
header, API-token requests are exempt — the bearer header itself proves
intent. Add Content-Type: application/json for JSON
bodies.
Every route is guarded by a named permission (e.g.
dns.sandbox, cert.request,
admin.devices.manage). When a token is used, Meridian
checks BOTH the user's current permissions AND the token's stored
scopes — the required permission must appear in both. Rotate or
revoke a token under Settings → API tokens if its
scopes drift out of date.
# Dig against an explicit resolver
curl -sS -X POST /api/v1/dns/dig \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"target":"example.com","record_type":"A","resolver":"1.1.1.1"}'
# Propagation across 16 public resolvers
curl -sS -X POST /api/v1/dns/propagation \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"target":"example.com","record_type":"A"}'
# Hop-trace recursion through a named resolver group
curl -sS -X POST /api/v1/dns/trace \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"target":"example.com","record_type":"A","group_tag":"corp"}'
POST /api/v1/network/ping {"target":"1.1.1.1","count":4}
POST /api/v1/network/traceroute {"target":"1.1.1.1"}
POST /api/v1/network/http-test {"url":"https://example.com"}
POST /api/v1/network/port-scan {"target":"1.1.1.1","ports":"22,80,443"}
POST /api/v1/network/ip-geolocate {"ip":"8.8.8.8"}
POST /api/v1/network/header-audit {"url":"https://example.com"}
GET /api/v1/network/interfaces
POST /api/v1/network/cve-lookup {"cve":"CVE-2021-44228"}
POST /api/v1/network/kev-lookup {"cve":"CVE-2021-44228"}
POST /api/v1/network/kev-search {"query":"log4j"}
POST /api/v1/network/epss-lookup {"cve":"CVE-2021-44228"}
POST /api/v1/network/circl-lookup {"cve":"CVE-2021-44228"}
POST /api/v1/network/ip-reputation {"ip":"203.0.113.42"}
POST /api/v1/network/dshield-lookup {"ip":"203.0.113.42"}
# API-key required:
POST /api/v1/network/abuseipdb-lookup {"ip":"203.0.113.42"}
POST /api/v1/network/greynoise-lookup {"ip":"203.0.113.42"}
POST /api/v1/network/virustotal-lookup {"target":"example.com"}
POST /api/v1/network/urlscan-search {"query":"domain:example.com"}
POST /api/v1/network/shodan-lookup {"ip":"203.0.113.42"}
POST /api/v1/network/censys-lookup {"ip":"203.0.113.42"}
GET /api/v1/certs/ # list all (portal + monitored)
POST /api/v1/certs/watchlist {"host":"example.com","port":443,
"notify_channels":["uuid…"],
"renew_before_days":30}
POST /api/v1/certs/{id}/refresh # re-fetch + threshold + fp-change
PATCH /api/v1/certs/{id} {"notify_channels":[…],
"renew_before_days":14,
"auto_renew":true}
POST /api/v1/certs/upload {"leaf_pem":"…","chain_pem":"…",
"cert_type":"internal"}
POST /api/v1/certs/csr {"subject_cn":"…","sans":[],
"key_type":"ecdsa_p256"}
DELETE /api/v1/certs/{id}
GET /api/v1/monitors/ # list
POST /api/v1/monitors/ {"name":"…","kind":"https",
"target":"api.example.com",
"interval_seconds":300,
"timeout_seconds":10}
PATCH /api/v1/monitors/{id} # edit any field
POST /api/v1/monitors/{id}/toggle # flip enabled
DELETE /api/v1/monitors/{id}
GET /api/v1/monitors/{id}/samples # recent samples
GET /api/v1/monitors/{id}/incidents # open + closed incidents
POST /api/v1/directory/user/search {"query":"jsmith","limit":25}
POST /api/v1/directory/group/search {"query":"Domain Admins","limit":25}
POST /api/v1/directory/integrations/{id}/test
POST /api/v1/wizards/run {"wizard_key":"ssl.deep_inspect",
"target":"example.com"}
Returns: wizard_key · target · outcome · steps[] · suggestions[].
Each step has name · outcome · message · detail.
GET /api/v1/runbooks # list
GET /api/v1/runbooks/{id} # detail + steps
POST /api/v1/runbooks {"name":"…","steps":[…]}
POST /api/v1/runbooks/{id}/run # execute all steps
GET /api/v1/runbooks/{id}/runs # run history
GET /api/v1/runbooks/{id}/runs/{rid} # full step_results
# Directory
GET /api/v1/admin/integrations/directory
POST /api/v1/admin/integrations/directory
PATCH /api/v1/admin/integrations/directory/{id}
DELETE /api/v1/admin/integrations/directory/{id}
# Threat Intel — keys + source toggles
GET /api/v1/admin/integrations/threat-intel
POST /api/v1/admin/integrations/threat-intel
PATCH /api/v1/admin/integrations/threat-intel/{id}
DELETE /api/v1/admin/integrations/threat-intel/{id}
GET /api/v1/admin/integrations/threat-intel-sources
PATCH /api/v1/admin/integrations/threat-intel-sources/{source_key}
GET /api/v1/admin/devices
POST /api/v1/admin/devices
PATCH /api/v1/admin/devices/{id}
DELETE /api/v1/admin/devices/{id}
POST /api/v1/admin/devices/{id}/backup # trigger on-demand backup
GET /api/v1/admin/devices/{id}/snapshots
GET /api/v1/admin/devices/{id}/snapshots/{sid}
GET /api/v1/admin/devices/{id}/snapshots/{a}/diff/{b}
Each token has a per-minute cap (default 120). Exceeding it returns
429 Too Many Requests. The UI session path is unlimited.
Errors follow the FastAPI default shape:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"detail":"API token is missing required scope(s): dns.sandbox"}
Validation failures from Pydantic surface 422 Unprocessable
Entity with a detail array of field errors.
Upstream-provider failures on Threat Intel routes surface
502 Bad Gateway. A missing Threat Intel key surfaces
412 Precondition Failed.
Under Settings → API tokens, each row has
Revoke (sets revoked_at; the token no
longer authenticates) and Delete (removes the row;
equivalent for auth purposes but cleans the list). Minting a new token
with the same name as a revoked one is allowed.
The raw OpenAPI 3.1 document is served at
/openapi.json — feed it into
Postman, Bruno, or an API-client generator. On airgapped deployments
(installer flag --airgapped) an interactive Swagger UI is
also mounted at /api-docs.