Monitors
A monitor checks one server's VPN/proxy tunnels — one or more protocols on the same server — from ExitProbe's probe network (or your own custom probes), verifying it connects, the exit IP matches what's expected, and reporting latency and uptime history.
/monitorsList your monitors with filtering, optional pagination, and fleet status counts. `data.summary` always counts the WHOLE filtered fleet (up / down / degraded / flapping / ip_mismatch / inactive), never just the page you asked for. Send `per_page` (and optionally `page`) to paginate; omit both and every monitor is returned.
| project_id | integer | Filter by project ID |
| protocol | string | Filter by protocol: wireguard, openvpn, ikev2, vless, hysteria, trojan, shadowsocks, vmess |
| status | string | Filter by fleet health: up, down, degraded, flapping, ip_mismatch, unknown |
| tag | string | Only monitors carrying this tag |
| per_page | integer | Page size, 1–100. Sending this (or page) turns pagination on and fills data.pagination. |
| page | integer | Page number, starting at 1 |
| defer_uptime | boolean | Skip the per-monitor uptime bars and the fleet chart — much faster for large fleets |
| lean | boolean | Drop stored configs and credentials from each row |
| search | string | Search by name, IP, or hostname |
| range | string | Uptime history range: 1h, 6h, 12h, 24h, 7d, 30d, 90d |
curl -X GET '/api/v1/monitors?status=up&range=24h' \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"data": {
"total": 2,
"summary": {
"total": 2,
"up": 2,
"down": 0,
"degraded": 0,
"flapping": 0,
"ip_mismatch": 0,
"unknown": 0,
"inactive": 0
},
"pagination": { "page": 1, "per_page": 25, "total": 2, "last_page": 1, "has_more": false },
"fleet_counts": {
"total": 2,
"up": 2,
"down": 0,
"ip_mismatch": 0
},
"fleet_uptime": {
"uptime_percentage": 99.8,
"avg_latency_ms": 45,
"total_checks": 288,
"incidents_count": 1,
"range": "24h",
"bars": [
{
"status": "up",
"percentage": 100,
"uptime_pct": 100.0,
"latency_ms": 42,
"avg_latency_ms": 42,
"checks_count": 144,
"label": "0%",
"timestamp_label": "00:00",
"tooltip": "Up - 100%",
"color": "green",
"color_class": "bg-green-500"
}
]
},
"monitors": [
{
"id": 1,
"project_id": 1,
"project_name": "Default",
"name": "VPN Exit Node 1",
"expected_exit_ip": "203.0.113.45",
"is_active": true,
"is_ip_verified": true,
"is_test": false,
"check_interval_seconds": 300,
"status": "up",
"fleet_health": {
"status": "up",
"label": "All Systems Operational",
"reason": "All enabled protocols are working correctly"
},
"protocols_summary": {
"wireguard": {
"enabled": true,
"status": "working",
"latency_ms": 42,
"last_check_at": "2026-08-22T10:30:00Z",
"last_exit_ip": "203.0.113.45"
},
"openvpn": {
"enabled": false,
"status": "not_configured",
"latency_ms": null
}
},
"uptime_history": {
"uptime_percentage": 99.8,
"avg_latency_ms": 42,
"total_checks": 144,
"incidents_count": 1,
"range": "24h",
"bars": []
},
"last_check_at": "2026-08-22T10:30:00Z",
"last_latency_ms": 42,
"last_exit_ip": "203.0.113.45",
"last_exit_location": "New York, US",
"exit_country_code": "US",
"flag_url": "https://flags.example.com/us.svg",
"wireguard_enabled": true,
"openvpn_enabled": false,
"ikev2_enabled": false,
"vless_enabled": false,
"hysteria_enabled": false,
"probe_mode": "all",
"probe_id": null,
"probe_ids": [],
"probes": [],
"single_probe": null
}
]
}
}/monitorsRegister a new VPN monitor. At least one protocol (WireGuard, OpenVPN, IKEv2, VLESS, Hysteria, Trojan, Shadowsocks, or VMess) must be enabled and configured, or the request fails with 422.
| name* | string | Monitor name (max 255 chars) |
| expected_exit_ip | string | Expected exit IP for verification (enables IP mismatch alerts). Max 64 chars. |
| protocol | string | Legacy single-protocol mode: one of wireguard, openvpn, ikev2, vless, hysteria, trojan, shadowsocks, vmess. When set, use the legacy config_content/uri/target_host/auth_* fields below instead of the per-protocol ones. |
| check_interval_seconds | integer | Check frequency in seconds. Minimum is plan-dependent, maximum is 3600. Defaults to your plan's minimum interval. |
| probe_id | integer | Single probe ID for dedicated monitoring (must exist in probes table; requires plan permission — see notes) |
| probe_ids | array<integer> | Array of probe IDs for custom subset monitoring (each must exist in probes table; requires plan permission — see notes) |
| wireguard_enabled | boolean | Enable WireGuard monitoring |
| wireguard_config | string | WireGuard client configuration file content (required if wireguard_enabled and protocol is not legacy-set) |
| openvpn_enabled | boolean | Enable OpenVPN monitoring |
| openvpn_config | string | OpenVPN client configuration file content |
| openvpn_auth_username | string | OpenVPN auth username, if the config requires separate credentials (max 255 chars) |
| openvpn_auth_password | string | OpenVPN auth password, if the config requires separate credentials |
| ikev2_enabled | boolean | Enable IKEv2 monitoring |
| ikev2_target_host | string | IKEv2 server address (required if ikev2_enabled). Max 255 chars. |
| ikev2_remote_id | string | IKEv2 remote ID / remote address, if different from target_host. Max 255 chars. |
| ikev2_auth_username | string | IKEv2 EAP identity (required if ikev2_enabled). Max 255 chars. |
| ikev2_auth_password | string | IKEv2 EAP secret (required if ikev2_enabled) |
| vless_enabled | boolean | Enable VLESS monitoring |
| vless_uri | string | Full vless:// connection URI (required if vless_enabled). Any URI is accepted — Reality, plain TLS, or unencrypted; raw TCP, WebSocket, gRPC, HTTPUpgrade, or XHTTP — not just Reality. |
| hysteria_enabled | boolean | Enable Hysteria 2 monitoring |
| hysteria_uri | string | Full hysteria2:// connection URI (required if hysteria_enabled) |
| trojan_enabled | boolean | Enable Trojan monitoring |
| trojan_uri | string | Full trojan:// connection URI (required if trojan_enabled) |
| shadowsocks_enabled | boolean | Enable Shadowsocks monitoring |
| shadowsocks_uri | string | Full ss:// connection URI (required if shadowsocks_enabled). SIP002 (plaintext or base64 userinfo) and legacy fully-base64-encoded forms are both accepted. |
| vmess_enabled | boolean | Enable VMess monitoring |
| vmess_uri | string | Full vmess:// connection URI (required if vmess_enabled) — the standard base64(JSON) share-link format |
| config_content | string | [Legacy] Config file content — only used when protocol is set directly (wireguard or openvpn) |
| uri | string | [Legacy] Connection URI — only used when protocol is set directly (vless or hysteria) |
| target_host | string | [Legacy] Server address — only used when protocol is set to ikev2. Max 255 chars. |
| target_port | integer | [Legacy] Server port (1–65535) |
| remote_id | string | [Legacy] Remote ID — only used when protocol is set to ikev2. Max 255 chars. |
| auth_username | string | [Legacy] Auth username — only used when protocol is set to ikev2 or openvpn. Max 255 chars. |
| auth_password | string | [Legacy] Auth password — only used when protocol is set to ikev2 or openvpn |
- •At least one of the *_enabled flags (or the matching config/URI field) must be set, or the request fails with a 422 "At least one VPN protocol must be configured."
- •Fails with 422 if your monitor count is already at or over your plan's monitor_limit.
- •check_interval_seconds has no fixed minimum — the floor is plan-dependent (getMinCheckIntervalSeconds()); the max is always 3600. Sending below your plan's floor returns a 422 naming the exact minimum.
- •probe_id / probe_ids are only honored if your plan allows manual probe selection (canSelectProbes()) — otherwise they're silently ignored and the monitor runs against the global probe pool.
- •The legacy config_content / uri / target_host / target_port / remote_id / auth_username / auth_password fields exist for single-protocol payloads (protocol set directly) — for multi-protocol monitors, prefer the per-protocol fields below (wireguard_config, ikev2_target_host, vless_uri, etc).
curl -X POST /api/v1/monitors \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
-H "Content-Type: application/json" \
-d '{
"name": "VPN Exit Node 1",
"expected_exit_ip": "203.0.113.45",
"check_interval_seconds": 300,
"wireguard_enabled": true,
"wireguard_config": "[Interface]\nPrivateKey=...",
"probe_mode": "all"
}'{
"success": true,
"message": "Server Monitor registered successfully. Probing will initiate shortly.",
"data": {
"monitor": {
"id": 1,
"user_id": 1,
"project_id": 1,
"name": "VPN Exit Node 1",
"protocol": "wireguard",
"expected_exit_ip": "203.0.113.45",
"check_interval_seconds": 300,
"status": "pending",
"is_active": true,
"wireguard_enabled": true,
"wireguard_config": "[Interface]\nPrivateKey=...",
"openvpn_enabled": false,
"ikev2_enabled": false,
"vless_enabled": false,
"hysteria_enabled": false,
"probe_id": null,
"probe_ids": [],
"created_at": "2026-08-22T10:30:00Z",
"updated_at": "2026-08-22T10:30:00Z"
},
"protocols": ["wireguard"]
}
}/monitors/{id}Retrieve detailed information about a specific monitor, including recent checks.
| range | string | Uptime history range: 1h, 6h, 12h, 24h, 7d, 30d, 90d |
curl -X GET /api/v1/monitors/1 \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"data": {
"monitor": {
"id": 1,
"user_id": 1,
"project_id": 1,
"name": "VPN Exit Node 1",
"protocol": "wireguard",
"expected_exit_ip": "203.0.113.45",
"is_active": true,
"is_ip_verified": true,
"check_interval_seconds": 300,
"status": "up",
"last_check_at": "2026-08-22T10:30:00Z",
"last_latency_ms": 42,
"last_exit_ip": "203.0.113.45",
"exit_country_code": "US",
"exit_location_display": "New York, US",
"wireguard_enabled": true,
"wireguard_config": "[Interface]\nPrivateKey=...",
"openvpn_enabled": false,
"ikev2_enabled": false,
"vless_enabled": false,
"hysteria_enabled": false
},
"is_active": true,
"is_ip_verified": true,
"ip_mismatch_details": null,
"fleet_health": {
"status": "up",
"label": "All Systems Operational",
"reason": "All enabled protocols are working correctly"
},
"protocols_summary": {
"wireguard": {
"enabled": true,
"status": "working",
"latency_ms": 42,
"last_check_at": "2026-08-22T10:30:00Z",
"last_exit_ip": "203.0.113.45"
}
},
"uptime_history": {
"uptime_percentage": 99.8,
"avg_latency_ms": 42,
"total_checks": 144,
"incidents_count": 1,
"range": "24h",
"bars": []
},
"flag_url": "https://flags.example.com/us.svg",
"checks": []
}
}/monitors/{id}Modify monitor configuration, protocols, check interval, probe assignment, or active/paused state. Accepts every body field Create Monitor does (all optional here) plus is_active.
| name | string | Monitor name (max 255 chars) |
| expected_exit_ip | string | Expected exit IP (max 64 chars) |
| check_interval_seconds | integer | Check interval in seconds (plan-dependent minimum, max 3600) |
| is_active | boolean | Pause (false) or resume (true) this monitor. Omitting it defaults to true — see notes. |
| status | string | One of up, down, pending, paused. Rarely set directly — status is normally driven by check results. |
| wireguard_enabled | boolean | Enable/disable WireGuard monitoring |
| wireguard_config | string | Updated WireGuard configuration |
| openvpn_enabled | boolean | Enable/disable OpenVPN monitoring |
| ikev2_enabled | boolean | Enable/disable IKEv2 monitoring |
| vless_enabled | boolean | Enable/disable VLESS monitoring |
| hysteria_enabled | boolean | Enable/disable Hysteria 2 monitoring |
| probe_id | integer | Reassign to a single probe (requires plan permission — see notes) |
| probe_ids | array<integer> | Reassign to a custom subset of probes (requires plan permission — see notes) |
- •Every field is optional — send only what you're changing. Fields you omit keep their current value, with one exception: is_active resets to true on every update unless you explicitly send is_active in the same request (this is how the "Reactivate" action works after an IP-mismatch deactivation).
- •Send is_active: false to pause a monitor (fires the monitor.paused webhook event); is_active: true resumes it (monitor.resumed). Changing any other field without touching is_active fires monitor.updated instead.
- •Updating always clears any existing IP-mismatch deactivation reason on the monitor.
- •probe_id / probe_ids are only honored if your plan allows manual probe selection (canSelectProbes()) or you're an admin.
- •See Create Monitor for the full set of per-protocol config/URI fields (openvpn_config, ikev2_target_host, vless_uri, hysteria_uri, etc.) — all of them are accepted here too.
curl -X PUT /api/v1/monitors/1 \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
-H "Content-Type: application/json" \
-d '{
"check_interval_seconds": 600
}'{
"success": true,
"message": "Server Monitor updated successfully.",
"data": {
"monitor": {
"id": 1,
"user_id": 1,
"project_id": 1,
"name": "VPN Exit Node 1",
"protocol": "wireguard",
"check_interval_seconds": 600,
"status": "up",
"updated_at": "2026-08-22T10:45:00Z"
},
"protocols_summary": {
"wireguard": {
"enabled": true,
"status": "working",
"latency_ms": 42
}
}
}
}/monitors/{id}Permanently delete a monitor and stop all checks.
curl -X DELETE /api/v1/monitors/1 \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"message": "Server Monitor deleted successfully."
}/monitors/{id}/check-nowImmediately dispatch diagnostic checks for all enabled protocols across assigned probes.
curl -X POST /api/v1/monitors/1/check-now \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"message": "Diagnostic checks dispatched to edge probes for all enabled protocols.",
"data": {
"dispatched_checks_count": 3,
"check_ids": [101, 102, 103]
}
}/monitors/{id}/check-statusPoll the lifecycle status of checks dispatched by check-now requests.
curl -X GET /api/v1/monitors/1/check-status \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"data": {
"checks": [
{
"id": 101,
"protocol": "wireguard",
"probe_id": 1,
"probe_name": "New York",
"probe_country_code": "US",
"status": "up",
"latency_ms": 42,
"packet_loss": "0%",
"exit_ip": "203.0.113.45",
"is_ip_mismatch": false,
"error_message": null,
"created_at": "2026-08-22T10:30:00Z",
"completed_at": "2026-08-22T10:30:05Z"
}
],
"pending_count": 0,
"completed_count": 1,
"total": 1,
"all_complete": true
}
}/monitors/{id}/checksRetrieve paginated history of all checks performed on a monitor.
| per_page | integer | Results per page (max 100, default 20) |
| page | integer | Page number (default 1) |
curl -X GET '/api/v1/monitors/1/checks?per_page=20' \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"data": {
"data": [
{
"id": 100,
"monitor_id": 1,
"probe_id": 1,
"probe_name": "New York",
"probe_city": "New York",
"probe_country": "United States",
"probe_country_code": "US",
"protocol": "wireguard",
"status": "up",
"latency_ms": 42,
"packet_loss": "0%",
"exit_ip": "203.0.113.45",
"exit_location": "New York, US",
"is_ip_mismatch": false,
"error_message": null,
"completed_at": "2026-08-22T10:30:00Z",
"created_at": "2026-08-22T10:30:00Z"
}
],
"links": {
"first": "/api/v1/monitors/1/checks?page=1",
"last": "/api/v1/monitors/1/checks?page=5",
"next": "/api/v1/monitors/1/checks?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 20,
"to": 20,
"total": 100
}
}
}/monitors/{id}/checksWipe a single monitor's check history — the monitor itself is kept, only its historical MonitorCheck rows are deleted. Irreversible.
- •This does not delete the monitor — use Delete Monitor for that.
- •Resets uptime percentage and every chart on the monitor detail page back to "no data" until new checks come in.
curl -X DELETE /api/v1/monitors/1/checks \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"message": "Cleared 1042 check record(s) for this monitor."
}/monitors/{id}/uptimeRetrieve segmented uptime timeline showing status bars across a time range.
| range | string | Time range: 1h, 6h, 12h, 24h, 7d, 30d, 90d (default 24h) |
| bars | integer | Number of bars to return (max 100, default 30) |
curl -X GET '/api/v1/monitors/1/uptime?range=24h&bars=48' \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"data": {
"monitor_id": 1,
"name": "VPN Exit Node 1",
"expected_exit_ip": "203.0.113.45",
"range": "24h",
"uptime_percentage": 99.8,
"avg_latency_ms": 45,
"total_checks": 288,
"incidents_count": 1,
"bars": [
{
"status": "up",
"percentage": 100,
"uptime_pct": 100.0,
"latency_ms": 42,
"avg_latency_ms": 42,
"checks_count": 144,
"label": "0%",
"timestamp_label": "00:00",
"tooltip": "Up - 100%",
"color": "green",
"color_class": "bg-green-500"
}
]
}
}/monitors/{id}/uptime/regionsPer-probe breakdown of checks within a single uptime bar's time window — powers "click for regional detail" under the uptime timeline. Pass the bar's own timestamp and the response's top-level bucket_seconds straight from a Get Uptime History call.
| start* | string (ISO 8601 datetime) | Window start — use one bar's timestamp field from Get Uptime History verbatim |
| bucket_seconds* | integer | Window length in seconds — use the uptime response's top-level bucket_seconds (1–604800) |
curl -X GET '/api/v1/monitors/1/uptime/regions?start=2026-08-23T17:00:00Z&bucket_seconds=3600' \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"data": {
"monitor_id": 1,
"window_start": "2026-08-23T17:00:00+00:00",
"window_end": "2026-08-23T18:00:00+00:00",
"total_checks": 12,
"up_checks": 12,
"avg_latency_ms": 47,
"regions": [
{
"probe_id": 3,
"probe_name": "London UK",
"location_city": "London",
"location_country": "United Kingdom",
"up": 12,
"total": 12,
"avg_latency_ms": 47,
"status": "up"
}
]
}
}/monitors/{id}/analyticsRetrieve comprehensive analytics including latency trends, protocol stats, incidents, and exit IP history for charting.
| range | string | Analytics range: 1h, 6h, 12h, 24h, 7d, 30d, 90d (default 24h) |
curl -X GET '/api/v1/monitors/1/analytics?range=7d' \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"data": {
"range": "7d",
"bucket_seconds": 3600,
"generated_at": "2026-08-22T10:30:00Z",
"summary": {
"uptime_pct": 99.8,
"avg_latency_ms": 45,
"p95_latency_ms": 85,
"min_latency_ms": 32,
"max_latency_ms": 120,
"total_checks": 2016,
"failed_checks": 4,
"incidents_count": 1,
"mttr_seconds": 300,
"longest_outage_seconds": 600,
"avg_packet_loss": 0.2,
"exit_ip_stability_pct": 100
},
"latency_series": [
{
"t": "2026-08-15T00:00:00Z",
"label": "Aug 15, 00:00",
"avg_ms": 44,
"min_ms": 32,
"max_ms": 85,
"p95_ms": 78,
"checks": 288
}
],
"latency_by_protocol": {
"wireguard": [
{
"t": "2026-08-15T00:00:00Z",
"label": "Aug 15, 00:00",
"value": 44,
"checks": 288
}
]
},
"uptime_series": [],
"packet_loss_series": [],
"checks_volume_series": [],
"status_distribution": {
"up": 2012,
"down": 4,
"timeout": 0,
"error": 0,
"ip_mismatch": 0,
"pending": 0
},
"protocol_stats": [
{
"protocol": "wireguard",
"label": "WireGuard",
"enabled": true,
"checks": 2016,
"up": 2012,
"down": 4,
"uptime_pct": 99.8,
"avg_latency_ms": 45,
"p95_latency_ms": 85,
"min_latency_ms": 32,
"max_latency_ms": 120,
"incidents": 1,
"last_check_at": "2026-08-22T10:30:00Z"
}
],
"probe_stats": [],
"incidents": [
{
"id": "inc_1",
"started_at": "2026-08-20T14:30:00Z",
"ended_at": "2026-08-20T14:35:00Z",
"duration_seconds": 300,
"is_ongoing": false,
"protocol": "wireguard",
"probe_id": 1,
"probe_name": "New York",
"reason": "Connection timeout",
"failed_checks": 2,
"kind": "timeout"
}
],
"exit_ip_history": [
{
"ip": "203.0.113.45",
"is_expected": true,
"count": 2016,
"first_seen": "2026-08-15T00:00:00Z",
"last_seen": "2026-08-22T10:30:00Z",
"location": "New York, US",
"country_code": "US"
}
],
"latency_heatmap": [],
"latency_distribution": [
{
"bucket_label": "0-50ms",
"from_ms": 0,
"to_ms": 50,
"count": 1600
}
]
}
}/monitors/summaryHow many monitors are up, down, degraded, flapping, IP-mismatched or inactive — without any rows, uptime bars or credentials. Cheap enough to poll from a status board.
| project_id | integer | Count only this project |
curl -X GET /api/v1/monitors/summary \
-H "Authorization: Bearer 1|K7f9XyZaQ2mN8pL4vR6tW1sJ0hD3cB5gA7eF9uI2oY4"{
"success": true,
"data": {
"total": 93,
"up": 88,
"down": 2,
"degraded": 1,
"flapping": 1,
"ip_mismatch": 1,
"unknown": 0,
"inactive": 1
}
}/monitors/{id}/statusLightweight status endpoint for third-party integrations showing current monitor health.
curl -X GET /api/v1/monitors/1/status \
-H "Authorization: Bearer ep_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"{
"success": true,
"data": {
"monitor_id": 1,
"name": "VPN Exit Node 1",
"status": "up",
"last_check_at": "2026-08-22T10:30:00Z",
"last_latency_ms": 42,
"last_exit_ip": "203.0.113.45",
"exit_country_code": "US",
"uptime_percentage": 99.8
}
}