API Status Codes Reference
Complete reference of all HTTP status codes returned by the BanGUI API v1.
Use this document to handle every possible response from every endpoint.
Status Code Taxonomy
| Code |
Meaning |
When Used |
| 200 |
OK |
Successful GET, PUT, POST (no creation) |
| 201 |
Created |
Successful POST that created a resource |
| 204 |
No Content |
Successful DELETE or PUT with no response body |
| 400 |
Bad Request |
Invalid input, validation failure, bad IP, URL validation |
| 401 |
Unauthorized |
Missing, expired, or invalid session |
| 404 |
Not Found |
Entity does not exist |
| 409 |
Conflict |
State conflict (already exists, already done, operation failed) |
| 422 |
Unprocessable Entity |
Request body validation failed (Pydantic) |
| 429 |
Too Many Requests |
Rate limit exceeded |
| 500 |
Internal Server Error |
Unexpected server failure |
| 502 |
Bad Gateway |
fail2ban socket unreachable |
| 503 |
Service Unavailable |
Setup incomplete or component degraded |
/api/v1/auth
POST /api/v1/auth/login
| Status |
Description |
Response Model |
| 200 |
Login successful |
LoginResponse |
| 401 |
Invalid password |
Error body |
| 422 |
Validation error — invalid request body |
Error body |
| 429 |
Too many login attempts, retry after delay |
Error body |
| 503 |
Setup not complete |
Error body |
GET /api/v1/auth/session
| Status |
Description |
Response Model |
| 200 |
Session valid |
SessionValidResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
POST /api/v1/auth/logout
| Status |
Description |
Response Model |
| 200 |
Logout successful |
LogoutResponse |
| 401 |
Session missing or invalid (silently successful) |
Error body |
/api/v1/setup
GET /api/v1/setup
| Status |
Description |
Response Model |
| 200 |
Setup status returned |
SetupStatusResponse |
POST /api/v1/setup
| Status |
Description |
Response Model |
| 201 |
Setup completed successfully |
SetupResponse |
| 400 |
Validation error in request body |
Error body |
| 409 |
Setup already completed |
Error body |
GET /api/v1/setup/timezone
| Status |
Description |
Response Model |
| 200 |
Timezone returned |
SetupTimezoneResponse |
/api/v1/health
GET /api/v1/health
| Status |
Description |
Response Model |
| 200 |
All components healthy |
HealthResponse |
| 503 |
fail2ban offline or component degraded |
HealthResponse |
/api/v1/dashboard
GET /api/v1/dashboard/status
| Status |
Description |
Response Model |
| 200 |
Server status returned |
ServerStatusResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/dashboard/bans
| Status |
Description |
Response Model |
| 200 |
Ban list returned |
DashboardBanListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/dashboard/bans/by-country
| Status |
Description |
Response Model |
| 200 |
Ban counts by country returned |
BansByCountryResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/dashboard/bans/trend
| Status |
Description |
Response Model |
| 200 |
Ban trend data returned |
BanTrendResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/dashboard/bans/by-jail
| Status |
Description |
Response Model |
| 200 |
Ban counts by jail returned |
BansByJailResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
/api/v1/bans
GET /api/v1/bans/active
| Status |
Description |
Response Model |
| 200 |
Active ban list returned |
ActiveBanListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/bans
| Status |
Description |
Response Model |
| 201 |
IP banned successfully |
JailCommandResponse |
| 400 |
Invalid IP address |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 409 |
Ban command failed in fail2ban |
Error body |
| 429 |
Rate limit exceeded for ban operations |
Error body |
| 502 |
fail2ban unreachable |
Error body |
DELETE /api/v1/bans
| Status |
Description |
Response Model |
| 200 |
IP unbanned successfully |
JailCommandResponse |
| 400 |
Invalid IP address |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 409 |
Unban command failed in fail2ban |
Error body |
| 429 |
Rate limit exceeded for unban operations |
Error body |
| 502 |
fail2ban unreachable |
Error body |
DELETE /api/v1/bans/all
| Status |
Description |
Response Model |
| 200 |
All bans cleared |
UnbanAllResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
/api/v1/jails
GET /api/v1/jails
| Status |
Description |
Response Model |
| 200 |
Jails list returned |
JailListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/jails/{name}
| Status |
Description |
Response Model |
| 200 |
Jail detail returned |
JailDetailResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/jails/reload-all
| Status |
Description |
Response Model |
| 200 |
All jails reloaded |
JailCommandResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 409 |
fail2ban reports operation failed |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/jails/{name}/start
| Status |
Description |
Response Model |
| 200 |
Jail started |
JailCommandResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 409 |
fail2ban reports operation failed |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/jails/{name}/stop
| Status |
Description |
Response Model |
| 200 |
Jail stopped |
JailCommandResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 409 |
fail2ban reports operation failed |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/jails/{name}/idle
| Status |
Description |
Response Model |
| 200 |
Idle mode toggled |
JailCommandResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 409 |
fail2ban reports operation failed |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/jails/{name}/reload
| Status |
Description |
Response Model |
| 200 |
Jail reloaded |
JailCommandResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 409 |
fail2ban reports operation failed |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/jails/{name}/ignoreip
| Status |
Description |
Response Model |
| 200 |
Ignore list returned |
IgnoreListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/jails/{name}/ignoreip
| Status |
Description |
Response Model |
| 201 |
IP added to ignore list |
JailCommandResponse |
| 400 |
IP or network invalid |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 409 |
fail2ban reports operation failed |
Error body |
| 502 |
fail2ban unreachable |
Error body |
DELETE /api/v1/jails/{name}/ignoreip
| Status |
Description |
Response Model |
| 200 |
IP removed from ignore list |
JailCommandResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 409 |
fail2ban reports operation failed |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/jails/{name}/ignoreself
| Status |
Description |
Response Model |
| 200 |
ignoreself toggled |
JailCommandResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 409 |
fail2ban reports operation failed |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/jails/{name}/banned
| Status |
Description |
Response Model |
| 200 |
Banned IPs returned |
JailBannedIpsResponse |
| 400 |
page or page_size out of range |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found |
Error body |
| 502 |
fail2ban unreachable |
Error body |
/api/v1/history
GET /api/v1/history
| Status |
Description |
Response Model |
| 200 |
History list returned |
HistoryListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/history/archive
| Status |
Description |
Response Model |
| 200 |
Archived history list returned |
HistoryListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/history/{ip}
| Status |
Description |
Response Model |
| 200 |
IP history detail returned |
IpDetailResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
No history found for this IP |
Error body |
| 502 |
fail2ban unreachable |
Error body |
/api/v1/geo
GET /api/v1/geo/lookup/{ip}
| Status |
Description |
Response Model |
| 200 |
IP lookup result returned |
IpLookupResponse |
| 400 |
Invalid IP address |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/geo/stats
| Status |
Description |
Response Model |
| 200 |
Geo cache stats returned |
GeoCacheStatsResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
POST /api/v1/geo/re-resolve
| Status |
Description |
Response Model |
| 200 |
Re-resolve result |
GeoReResolveResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
/api/v1/server
GET /api/v1/server/settings
| Status |
Description |
Response Model |
| 200 |
Server settings returned |
ServerSettingsResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
PUT /api/v1/server/settings
| Status |
Description |
Response Model |
| 204 |
Settings updated successfully |
No body |
| 400 |
Set command rejected by fail2ban |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/server/flush-logs
| Status |
Description |
Response Model |
| 200 |
Logs flushed successfully |
FlushLogsResponse |
| 400 |
Command rejected by fail2ban |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
/api/v1/config
GET /api/v1/config/global
| Status |
Description |
Response Model |
| 200 |
Global config returned |
GlobalConfigResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
PUT /api/v1/config/global
| Status |
Description |
Response Model |
| 204 |
Global config updated successfully |
No body |
| 400 |
Set command rejected or log_target invalid |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 429 |
Rate limit exceeded for config update operations |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/config/reload
| Status |
Description |
Response Model |
| 204 |
Fail2ban reloaded successfully |
No body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 409 |
Reload command failed in fail2ban |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/config/restart
| Status |
Description |
Response Model |
| 204 |
Fail2ban restarted successfully |
No body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 409 |
Stop command failed in fail2ban |
Error body |
| 502 |
fail2ban unreachable for stop command |
Error body |
| 503 |
fail2ban did not come back online within 10s |
Error body |
POST /api/v1/config/regex-test
| Status |
Description |
Response Model |
| 200 |
Regex test result |
RegexTestResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 422 |
Invalid regex pattern |
Error body |
POST /api/v1/config/preview-log
| Status |
Description |
Response Model |
| 200 |
Log preview result |
LogPreviewResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 422 |
Invalid regex pattern |
Error body |
GET /api/v1/config/map-color-thresholds
| Status |
Description |
Response Model |
| 200 |
Color thresholds returned |
MapColorThresholdsResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
PUT /api/v1/config/map-color-thresholds
| Status |
Description |
Response Model |
| 200 |
Color thresholds updated |
MapColorThresholdsResponse |
| 400 |
Validation error (thresholds not properly ordered) |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 429 |
Rate limit exceeded for config update operations |
Error body |
GET /api/v1/config/fail2ban-log
| Status |
Description |
Response Model |
| 200 |
Log file lines returned |
Fail2BanLogResponse |
| 400 |
Log target not a file or path outside allowed directory |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/config/service-status
| Status |
Description |
Response Model |
| 200 |
Service status returned |
ServiceStatusResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
/api/v1/config/jails (jail_config router)
GET /api/v1/config/jails
| Status |
Description |
Response Model |
| 200 |
Jails config list returned |
JailConfigListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/config/jails/{name}
| Status |
Description |
Response Model |
| 200 |
Jail config detail returned |
JailConfigDetailResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found in config |
Error body |
| 502 |
fail2ban unreachable |
Error body |
PUT /api/v1/config/jails/{name}
| Status |
Description |
Response Model |
| 200 |
Jail config updated |
JailConfigDetailResponse |
| 400 |
Invalid value for a property |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found in config |
Error body |
| 422 |
Validation error |
Error body |
| 429 |
Rate limit exceeded for jail config operations |
Error body |
| 502 |
fail2ban unreachable |
Error body |
POST /api/v1/config/jails/{name}/commit
| Status |
Description |
Response Model |
| 200 |
Changes committed successfully |
JailConfigDetailResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found in config |
Error body |
| 409 |
Commit failed (fail2ban rejected the new config) |
Error body |
| 429 |
Rate limit exceeded for jail config operations |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/config/jails/{name}/rollback
| Status |
Description |
Response Model |
| 200 |
Rollback successful |
JailConfigDetailResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found in config |
Error body |
| 502 |
fail2ban unreachable |
Error body |
DELETE /api/v1/config/jails/{name}
| Status |
Description |
Response Model |
| 204 |
Jail deleted successfully |
No body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found in config |
Error body |
| 409 |
Jail is a shipped default (conf-only) |
Error body |
| 429 |
Rate limit exceeded for jail config operations |
Error body |
POST /api/v1/config/jails
| Status |
Description |
Response Model |
| 201 |
Jail created |
JailConfigDetailResponse |
| 400 |
Invalid jail name |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 409 |
Jail already exists |
Error body |
| 429 |
Rate limit exceeded for jail config operations |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/config/jails/{name}/files
| Status |
Description |
Response Model |
| 200 |
Config files returned |
ConfigFileListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Jail not found in config |
Error body |
/api/v1/config/filters (filter_config router)
GET /api/v1/config/filters
| Status |
Description |
Response Model |
| 200 |
Filter list returned |
FilterListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/config/filters/{name}
| Status |
Description |
Response Model |
| 200 |
Filter config returned |
FilterConfig |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Filter not found in filter.d/ |
Error body |
| 502 |
fail2ban unreachable |
Error body |
PUT /api/v1/config/filters/{name}
| Status |
Description |
Response Model |
| 200 |
Filter updated |
FilterConfig |
| 400 |
Invalid filter name |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Filter not found |
Error body |
| 422 |
Regex pattern failed to compile |
Error body |
| 429 |
Rate limit exceeded for filter update operations |
Error body |
| 500 |
Failed to write .local file |
Error body |
POST /api/v1/config/filters
| Status |
Description |
Response Model |
| 201 |
Filter created |
FilterConfig |
| 400 |
Invalid filter name or regex too long |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 409 |
Filter already exists |
Error body |
| 422 |
Regex pattern failed to compile |
Error body |
| 429 |
Rate limit exceeded for filter create operations |
Error body |
| 500 |
Failed to write .local file |
Error body |
DELETE /api/v1/config/filters/{name}
| Status |
Description |
Response Model |
| 204 |
Filter deleted successfully |
No body |
| 400 |
Invalid filter name |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Filter not found |
Error body |
| 409 |
Filter is a shipped default (conf-only) |
Error body |
| 429 |
Rate limit exceeded for filter delete operations |
Error body |
| 500 |
Failed to delete .local file |
Error body |
/api/v1/config/actions (action_config router)
GET /api/v1/config/actions
| Status |
Description |
Response Model |
| 200 |
Action list returned |
ActionListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 502 |
fail2ban unreachable |
Error body |
GET /api/v1/config/actions/{name}
| Status |
Description |
Response Model |
| 200 |
Action config returned |
ActionConfig |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Action not found in action.d/ |
Error body |
| 502 |
fail2ban unreachable |
Error body |
PUT /api/v1/config/actions/{name}
| Status |
Description |
Response Model |
| 200 |
Action updated |
ActionConfig |
| 400 |
Invalid action name |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Action not found |
Error body |
| 429 |
Rate limit exceeded for action update operations |
Error body |
| 500 |
Failed to write .local file |
Error body |
POST /api/v1/config/actions
| Status |
Description |
Response Model |
| 201 |
Action created |
ActionConfig |
| 400 |
Invalid action name |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 409 |
Action already exists |
Error body |
| 429 |
Rate limit exceeded for action create operations |
Error body |
| 500 |
Failed to write .local file |
Error body |
DELETE /api/v1/config/actions/{name}
| Status |
Description |
Response Model |
| 204 |
Action deleted successfully |
No body |
| 400 |
Invalid action name |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Action not found |
Error body |
| 409 |
Action is a shipped default (conf-only) |
Error body |
| 429 |
Rate limit exceeded for action delete operations |
Error body |
| 500 |
Failed to delete .local file |
Error body |
/api/v1/blocklists
GET /api/v1/blocklists
| Status |
Description |
Response Model |
| 200 |
Blocklist sources returned |
BlocklistListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
POST /api/v1/blocklists
| Status |
Description |
Response Model |
| 201 |
Blocklist source created |
BlocklistSource |
| 400 |
URL validation failed |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
POST /api/v1/blocklists/import
| Status |
Description |
Response Model |
| 200 |
Import completed |
ImportRunResult |
| 401 |
Session missing, expired, or invalid |
Error body |
| 429 |
Rate limit exceeded for blocklist import |
Error body |
GET /api/v1/blocklists/schedule
| Status |
Description |
Response Model |
| 200 |
Schedule info returned |
ScheduleInfo |
| 401 |
Session missing, expired, or invalid |
Error body |
PUT /api/v1/blocklists/schedule
| Status |
Description |
Response Model |
| 200 |
Schedule updated |
ScheduleInfo |
| 401 |
Session missing, expired, or invalid |
Error body |
GET /api/v1/blocklists/log
| Status |
Description |
Response Model |
| 200 |
Import log returned |
ImportLogListResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
GET /api/v1/blocklists/{source_id}
| Status |
Description |
Response Model |
| 200 |
Blocklist source returned |
BlocklistSource |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Blocklist source not found |
Error body |
PUT /api/v1/blocklists/{source_id}
| Status |
Description |
Response Model |
| 200 |
Blocklist source updated |
BlocklistSource |
| 400 |
URL validation failed |
Error body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Blocklist source not found |
Error body |
DELETE /api/v1/blocklists/{source_id}
| Status |
Description |
Response Model |
| 204 |
Blocklist source deleted successfully |
No body |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Blocklist source not found |
Error body |
GET /api/v1/blocklists/{source_id}/preview
| Status |
Description |
Response Model |
| 200 |
Blocklist preview returned |
PreviewResponse |
| 401 |
Session missing, expired, or invalid |
Error body |
| 404 |
Blocklist source not found |
Error body |
| 502 |
URL could not be reached |
Error body |
Error Response Format
All error responses follow this structure:
Common error_code values
| code |
Meaning |
not_found |
Requested entity does not exist |
invalid_input |
Validation failure or bad parameters |
conflict |
State conflict (already exists, already done) |
authentication_required |
Session missing or invalid |
rate_limit_exceeded |
Rate limit hit — check retry_after_seconds in metadata |
fail2ban_unreachable |
fail2ban socket cannot be reached |
config_validation_failed |
Config value rejected |
config_file_not_found |
Config file does not exist |
jail_not_found |
Jail does not exist |
filter_not_found |
Filter does not exist |
action_not_found |
Action does not exist |
blocklist_source_not_found |
Blocklist source does not exist |
setup_already_complete |
Setup has already been run |
Status Code Decision Guide
Frontend gets 400 — what's wrong?
- Has
code: "invalid_input" → validation failure, check detail
- Has
code: "jail_not_found" → jail doesn't exist
- Has
code: "config_validation_failed" → config value rejected
Frontend gets 502 — what's wrong?
- fail2ban is down or socket path wrong
- Check
code: "fail2ban_unreachable"
Frontend gets 503 — what's wrong?
- Setup not complete (
code: "setup_already_complete")
- Health check: fail2ban offline or component degraded
Frontend gets 409 — what's wrong?
- Already done: jail already active/inactive, setup already complete
- Operation failed: fail2ban rejected the command
- Conflict: resource already exists
Frontend gets 429 — what's wrong?
- Rate limit exceeded
metadata.retry_after_seconds tells you how long to wait