Server Sources

Point ExitProbe at your own inventory API and it'll keep your monitors in sync automatically — no manual create-a-monitor step every time you spin up a new server.

How it works

A server source is your own HTTP API endpoint (a VPN-panel backend, an inventory system, whatever returns a JSON list of your servers). ExitProbe polls it on the schedule you set (sync_interval_minutes) and reconciles the result into monitors: new servers become new monitors, existing ones (matched by your external_key mapping) get updated, and servers that disappear from a later sync are handled per your prune_missing setting.

There are two ways to map your JSON shape onto a monitor:

flat mode (default)

One upstream item = one protocol = one monitor.

{ "hostname": "node-1", "type": "wireguard", "config": "[Interface]..." }

multi_protocol mode

One upstream item = one server, several protocols nested inside.

{ "hostname": "node-1", "protocols": { "wg": {...}, "ovpn": {...} } }
Always call Test Server Source before Create/Update in a UI you’re building — it fetches your URL and reports the detected keys without touching a single monitor, so you can verify your field_mapping paths first.
headers values (e.g. an Authorization header for your own API) are stored encrypted at rest. They’re used only to authenticate ExitProbe’s request to your base_url.
GET/server-sources
List Server Sources

Lists every server source on the account, plus your account's max (10).

Example Request
curl -X GET "https://api.exitprobe.com/api/v1/server-sources" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"
Example Response
{ "success": true, "data": { "sources": [ { "id": 1, "name": "My Panel", "base_url": "https://panel.example.com/api/servers", "sync_interval_minutes": 60, "monitors_count": 42, "last_synced_at": "2026-09-26T00:00:00Z", "last_sync_status": "success" } ], "max_sources": 10 } }
POST/server-sources
Create Server Source

Registers an external API endpoint (your own VPN-panel or inventory backend) that ExitProbe polls on a schedule and reconciles into monitors — creating, updating, and (per prune_missing) pausing or deleting monitors to match.

Request Body
name*stringMax 255 chars.
base_url*stringYour API endpoint. Must be a public URL. Max 2048 chars.
headersarrayUp to 10 `{key, value}` rows, sent as request headers when fetching base_url.
field_mapping*objectSee notes — shape depends on field_mapping.mode.
field_mapping.modestring`flat` (default) or `multi_protocol`.
field_mapping.name*stringDot-notation path to the monitor name in each upstream item, e.g. "hostname".
field_mapping.protocol*stringPath to the protocol value (flat mode only).
field_mapping.config_or_uri*stringPath to the config/URI value (flat mode only).
field_mapping.external_keystringPath to a stable id used to match this row to an existing monitor across syncs.
field_mapping.expected_exit_ipstring
field_mapping.protocols_keystringmulti_protocol mode: path to the object holding per-protocol data. Default "protocols".
sync_interval_minutes*integerOne of: 5, 15, 30, 60, 360, 1440.
check_interval_secondsintegerFor monitors this source creates. Plan-dependent minimum, max 3600.
project_idintegerMust belong to your account. Falls back to your default/first project if omitted.
prune_missingstring`none` (default), `pause`, or `delete` — what happens to a previously-synced monitor that disappears from a later sync.
is_activebooleanDefault true.
Notes
  • •Maximum 10 server sources per account (422 "You may have at most 10 server sources." past that).
  • •`base_url` must resolve to a public address — internal, loopback, and link-local addresses are rejected (SSRF guard), checked again at every sync in case DNS changed.
  • •`headers` are for authenticating to your own API (e.g. an `Authorization` header) — stored encrypted at rest.
  • •Two field-mapping modes: `flat` (one upstream JSON item = one protocol = one monitor) or `multi_protocol` (one upstream item = one server, with several protocols nested under one key, e.g. `protocols: {wg: {...}, ovpn: {...}}`).
  • •New monitors created by sync still count against your plan's `monitor_limit` — once hit, further rows are skipped (reported as `skipped_limit` in the sync result), not created.
Example Request
curl -X POST /api/v1/server-sources \
  -H "Authorization: Bearer 1|K7f9XyZaQ2mN8pL4vR6tW1sJ0hD3cB5gA7eF9uI2oY4" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Panel",
    "base_url": "https://panel.example.com/api/servers",
    "headers": [{"key": "Authorization", "value": "Bearer xyz"}],
    "field_mapping": {
      "mode": "flat",
      "name": "hostname",
      "protocol": "type",
      "config_or_uri": "config",
      "external_key": "id"
    },
    "sync_interval_minutes": 60,
    "prune_missing": "pause"
  }'
Example Response
{
  "success": true,
  "data": {
    "id": 1, "name": "My Panel", "base_url": "https://panel.example.com/api/servers",
    "field_mapping": { "mode": "flat", "name": "hostname", "protocol": "type", "config_or_uri": "config" },
    "sync_interval_minutes": 60, "check_interval_seconds": 60, "is_active": true,
    "prune_missing": "pause", "project_id": 3, "project_name": "Default Project",
    "monitors_count": 0, "last_synced_at": null, "last_sync_status": null, "last_sync_error": null,
    "created_at": "2026-09-26T00:00:00Z"
  }
}
GET/server-sources/{source}
Get Server Source

Includes the last 10 sync run summaries (recent_runs) in addition to the source itself.

Example Request
curl -X GET "https://api.exitprobe.com/api/v1/server-sources/{source}" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"
Example Response
{
  "success": true,
  "data": {
    "id": 1, "name": "My Panel", "monitors_count": 42,
    "recent_runs": [
      { "ran_at": "2026-09-26T00:00:00Z", "status": "success", "total": 40, "created": 2, "updated": 38, "skipped_invalid": 0, "skipped_limit": 0, "pruned": 0, "paused": 0 }
    ]
  }
}
PUT/server-sources/{source}
Update Server Source

Same body as Create. Omitting `headers` entirely keeps the stored values; sending `[]` clears them.

Example Request
curl -X PUT "https://api.exitprobe.com/api/v1/server-sources/{source}" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"
DELETE/server-sources/{source}
Delete Server Source

Removes the source. Monitors it previously synced are kept and just detached (server_source_id set null) — never deleted by this call.

Example Request
curl -X DELETE "https://api.exitprobe.com/api/v1/server-sources/{source}" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"
Example Response
{ "success": true, "message": "Server source removed. Synced monitors were kept and detached." }
POST/server-sources/{source}/test
Test Server Source

Fetches base_url and reports what it found — item count, detected field keys — without creating or touching any monitors. Always returns HTTP 200, even on a fetch failure (check `data.ok`).

Example Request
curl -X POST "https://api.exitprobe.com/api/v1/server-sources/{source}/test" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"
Example Response
{
  "success": true,
  "data": {
    "ok": true, "total": 40,
    "detected_keys": ["hostname", "ip", "protocols"],
    "detected_protocol_containers": { "protocols": ["wg", "ovpn"] },
    "sample": [ { "hostname": "node-1", "ip": "203.0.113.1" } ]
  }
}
POST/server-sources/{source}/sync
Sync Now

Runs a sync immediately instead of waiting for the schedule. Returns HTTP 200 if the fetch succeeded (even with some rows skipped — see `status: "partial"`), HTTP 502 if the fetch itself failed.

Example Request
curl -X POST "https://api.exitprobe.com/api/v1/server-sources/{source}/sync" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"
Example Response
{
  "success": true,
  "data": {
    "result": {
      "ok": true, "status": "success",
      "total": 40, "created": 2, "updated": 38, "skipped_invalid": 0, "skipped_limit": 0, "pruned": 0, "paused": 0,
      "errors": []
    },
    "source": { "id": 1, "last_synced_at": "2026-09-26T00:05:00Z", "last_sync_status": "success" }
  }
}