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": {...} } }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./server-sourcesLists every server source on the account, plus your account's max (10).
curl -X GET "https://api.exitprobe.com/api/v1/server-sources" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"{ "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 } }/server-sourcesRegisters 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.
| name* | string | Max 255 chars. |
| base_url* | string | Your API endpoint. Must be a public URL. Max 2048 chars. |
| headers | array | Up to 10 `{key, value}` rows, sent as request headers when fetching base_url. |
| field_mapping* | object | See notes — shape depends on field_mapping.mode. |
| field_mapping.mode | string | `flat` (default) or `multi_protocol`. |
| field_mapping.name* | string | Dot-notation path to the monitor name in each upstream item, e.g. "hostname". |
| field_mapping.protocol* | string | Path to the protocol value (flat mode only). |
| field_mapping.config_or_uri* | string | Path to the config/URI value (flat mode only). |
| field_mapping.external_key | string | Path to a stable id used to match this row to an existing monitor across syncs. |
| field_mapping.expected_exit_ip | string | |
| field_mapping.protocols_key | string | multi_protocol mode: path to the object holding per-protocol data. Default "protocols". |
| sync_interval_minutes* | integer | One of: 5, 15, 30, 60, 360, 1440. |
| check_interval_seconds | integer | For monitors this source creates. Plan-dependent minimum, max 3600. |
| project_id | integer | Must belong to your account. Falls back to your default/first project if omitted. |
| prune_missing | string | `none` (default), `pause`, or `delete` — what happens to a previously-synced monitor that disappears from a later sync. |
| is_active | boolean | Default true. |
- •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.
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"
}'{
"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"
}
}/server-sources/{source}Includes the last 10 sync run summaries (recent_runs) in addition to the source itself.
curl -X GET "https://api.exitprobe.com/api/v1/server-sources/{source}" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"{
"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 }
]
}
}/server-sources/{source}Same body as Create. Omitting `headers` entirely keeps the stored values; sending `[]` clears them.
curl -X PUT "https://api.exitprobe.com/api/v1/server-sources/{source}" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"/server-sources/{source}Removes the source. Monitors it previously synced are kept and just detached (server_source_id set null) — never deleted by this call.
curl -X DELETE "https://api.exitprobe.com/api/v1/server-sources/{source}" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"{ "success": true, "message": "Server source removed. Synced monitors were kept and detached." }/server-sources/{source}/testFetches 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`).
curl -X POST "https://api.exitprobe.com/api/v1/server-sources/{source}/test" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"{
"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" } ]
}
}/server-sources/{source}/syncRuns 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.
curl -X POST "https://api.exitprobe.com/api/v1/server-sources/{source}/sync" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"{
"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" }
}
}