API cookbook¶
Practical snippets for common tasks.
1. Create host + generate agent token¶
JWT="eyJhbGc..."
BASE="https://your-domain.tld/api/v1"
# Create host ("hostname" is the required field, not "name" —
# the worker correlates check results via (tenant_id, hostname))
HOST=$(curl -s -X POST "$BASE/hosts/" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"hostname": "web03.acme.local",
"tenant_id": "uuid-of-tenant",
"profile_id": "uuid-of-linux-profile",
"ip_address": "10.10.5.13",
"monitoring_mode": "agent",
"tags": ["production", "web"]
}')
HOST_ID=$(echo "$HOST" | jq -r .id)
# Generate agent token
TOKEN=$(curl -s -X POST "$BASE/hosts/$HOST_ID/agent-token" \
-H "Authorization: Bearer $JWT" | jq -r .token)
# Install agent
ssh root@web03.acme.local \
"wget -qO- https://your-domain.tld/agent/install.sh | bash -s -- $TOKEN https://your-domain.tld"
import requests
BASE = "https://your-domain.tld/api/v1"
HEADERS = {"Authorization": f"Bearer {JWT}"}
# Create host
r = requests.post(f"{BASE}/hosts/", json={
"hostname": "web03.acme.local",
"tenant_id": tenant_id,
"profile_id": linux_profile_id,
"ip_address": "10.10.5.13",
"monitoring_mode": "agent",
"tags": ["production", "web"],
}, headers=HEADERS)
host = r.json()
# Agent token
token = requests.post(
f"{BASE}/hosts/{host['id']}/agent-token",
headers=HEADERS,
).json()["token"]
print("Token:", token)
The response of POST /hosts/ also contains skipped_checks: checks that
the chosen profile_id would have added but couldn't be created because of
the mode (e.g. an agent check without monitoring_mode: "agent") — never a
silent drop, always visible in the body.
Instead of a local profile_id, the endpoint also accepts community_id
(the ID of a profile in the Vesana library, see
Vesana library): the server installs it
automatically if needed and binds the host to it.
2. Bulk import from CSV¶
import csv, requests
with open("hosts.csv") as f:
for row in csv.DictReader(f):
requests.post(f"{BASE}/hosts/", json={
"hostname": row["hostname"],
"tenant_id": row["tenant_id"],
"profile_id": row["profile_id"],
"ip_address": row["ip"],
}, headers=HEADERS)
3. Add service to host¶
curl -X POST "$BASE/host-services/" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"host_id": "host-uuid",
"tenant_id": "tenant-uuid",
"profile_check_id": "profile-check-uuid",
"display_name": "Disk /var",
"config_overrides": { "path": "/var" },
"interval_override": 60
}'
profile_check_id (from a device profile) and check_type (a free check
without a profile, an "inline check" with config_overrides carrying the
full check config) are mutually exclusive — set exactly one.
4. Create a dashboard programmatically¶
The dashboard belongs to the user whose token makes the request — not to a tenant; a tenant_id in the body is ignored. If the title is taken, Vesana appends a number ("Acme NOC 2").
The API takes exactly the config structure the web app writes. The easiest way is to build a dashboard once in the web app, read it with GET /dashboards/{id} and use its config as a template (or copy it with POST /dashboards/{id}/duplicate). Minimal example with a table of one tenant's checks:
dashboard = requests.post(f"{BASE}/dashboards/", json={
"title": "Acme NOC",
"config": {
"schemaVersion": 2,
"timeSettings": { "from": "now-24h", "to": "now", "refreshInterval": 0 },
"widgets": {
"w1": {
"type": "table",
"title": "Acme checks",
"dataSource": { "type": "status_table", "tenant_ids": [tenant_id] },
"options": {}
}
},
"layout": {
"lg": [
{ "i": "w1", "x": 0, "y": 0, "w": 24, "h": 10 }
]
}
}
}, headers=HEADERS).json()
The grid is 24 columns wide. Widget types: summary_state, summary_problems, summary_devices, summary_inventory, summary_activity, summary_noisiest, summary_tenants, stat, gauge, line_chart, bar_chart, top_n, table, heatmap, check_history, status_timeline, log_stream, system_log, anomaly_list, prediction_list, text, link_button. Older type names (status_overview, pie_chart, host_grid) are mapped to the current ones on read.
5. Bulk tag update¶
GET /hosts/ returns a JSON array (paged, at most 500 per request) and has no tag filter — filter in the script:
offset = 0
while True:
page = requests.get(f"{BASE}/hosts/", params={"limit": 500, "offset": offset}, headers=HEADERS).json()
for h in page:
if "legacy" in h["tags"]:
new_tags = [t for t in h["tags"] if t != "legacy"] + ["deprecated"]
requests.patch(f"{BASE}/hosts/{h['id']}", json={"tags": new_tags}, headers=HEADERS)
if len(page) < 500:
break
offset += 500
6. Secure a webhook receiver¶
The Webhook channel type sends a JSON POST without a signature and without custom headers. To secure it, append a secret to the channel's webhook URL (e.g. https://tickets.example.com/vesana-webhook?token=…) and check it in the receiver:
import hmac
from flask import Flask, request
WEBHOOK_TOKEN = "<token from the channel's webhook URL>"
app = Flask(__name__)
@app.route("/vesana-webhook", methods=["POST"])
def receive():
if not hmac.compare_digest(request.args.get("token", ""), WEBHOOK_TOKEN):
return "forbidden", 403
payload = request.get_json()
if payload["status"] == "CRITICAL":
send_pager(payload["host_name"], payload["service_name"])
return "ok", 200
Body fields: type, host_name, host_ip, service_name, status, previous_status, message, triggered_at, tenant_name, dashboard_url, plus duration_seconds and extra_data depending on the event.
7. Pull SLA report¶
Part of the "Reports (PDF + SLA)" preview feature — until it is enabled the endpoint answers 404.
curl "$BASE/sla/tenants/acme-uuid/sla-report?start=2026-04-01T00:00:00Z&end=2026-05-01T00:00:00Z" \
-H "Authorization: Bearer $JWT" \
| jq '.'
8. Set + cancel maintenance¶
# Schedule maintenance for a host
curl -X POST "$BASE/downtimes/" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"host_id": "host-uuid",
"start_at": "2026-04-30T22:00:00Z",
"end_at": "2026-04-30T23:30:00Z",
"comment": "Kernel update"
}'
# Recurring instead of one-off: add "recurrence" (an RRULE string), e.g.
# "FREQ=WEEKLY;BYDAY=MO,WE;UNTIL=20260801T220000Z"
# For multiple checks at once: POST $BASE/downtimes/bulk
# with { "service_ids": [...], "start_at": ..., "end_at": ... }
# Cancel early
curl -X DELETE "$BASE/downtimes/<id>" \
-H "Authorization: Bearer $JWT"
9. Get AI service analysis¶
10. Export the audit log for compliance¶
GET /audit/ returns JSON (at most 500 entries per request, total in X-Total-Count). For a CSV file, fetch page by page and convert:
OFFSET=0
while :; do
PAGE=$(curl -s "$BASE/audit/?created_from=2026-04-01T00:00:00Z&created_to=2026-05-01T00:00:00Z&limit=500&offset=$OFFSET" \
-H "Authorization: Bearer $JWT")
echo "$PAGE" | jq -r '.[] | [.created_at, .actor_email, .action, .target_type, .target_id] | @csv'
[ "$(echo "$PAGE" | jq length)" -lt 500 ] && break
OFFSET=$((OFFSET + 500))
done > audit-april.csv