Error Handling
The MySSL API uses conventional HTTP status codes to indicate success or failure, and returns a JSON body describing what went wrong. This page documents the exact status codes and response shapes the API produces so you can handle every failure mode programmatically.
Error Response Format
Every API error returns a JSON object with an error field containing a
human-readable message:
{
"error": "Domain not found"
}
Some errors include additional context fields alongside error — for example, tier-limit
errors include your current usage and an upgrade URL, and rate-limit errors include your quota and
reset time. These extra fields are documented per status code below.
HTTP Status Codes
| Code | Meaning | Common causes |
|---|---|---|
200 |
OK | Request succeeded. |
201 |
Created | Resource created (e.g. domain added, API key created). |
202 |
Accepted | Scan queued for asynchronous processing — poll for results. |
400 |
Bad Request | Missing or invalid parameters (e.g. no hostname, malformed date). |
401 |
Unauthorized | Missing, invalid or expired API key or JWT token. |
403 |
Forbidden | Tier limit reached, feature not on your plan, unverified domain, or inactive account. |
404 |
Not Found | Resource doesn't exist or belongs to another account; unknown endpoint. |
405 |
Method Not Allowed | Wrong HTTP method for the endpoint (e.g. PUT instead of PATCH). |
409 |
Conflict | Resource already exists (e.g. domain already being monitored). |
429 |
Too Many Requests | Daily API quota exhausted, or a per-endpoint burst limit hit. |
500 |
Internal Server Error | Something went wrong on our side — safe to retry with backoff. |
401 — Authentication Errors
All /api/v1 endpoints (except /auth/login and /auth/refresh)
require a valid API key in the Authorization: Bearer header. A 401 means the
request never reached your data — fix the credential and retry. The error message tells
you which check failed:
| Message | Cause |
|---|---|
| API key required | No Authorization header, or it doesn't use the Bearer scheme. |
| Invalid API key | The key doesn't exist, doesn't match, or has been revoked. |
| API key expired | The key was created with an expiry date that has passed. Create a new key. |
{
"error": "Invalid API key"
}
The JWT endpoints return their own 401 messages: Invalid email or password
(login), and Refresh token expired / Invalid refresh token (refresh).
Getting 401 with a key you believe is valid? Check that you're sending
Authorization: Bearer myssl_k_... exactly — see
Authentication.
403 — Permission & Tier Errors
A 403 means your credentials are valid but the action isn't allowed. Unlike
401, retrying won't help — you need to change something (verify a domain, remove a
resource, or upgrade your plan). Tier-related 403 responses include extra fields:
{
"error": "Domains limit reached (2). Upgrade your plan.",
"limit": 2,
"current": 2,
"remaining": 0,
"tier": "free",
"upgrade_url": "/dashboard/billing#upgrade"
}
Other 403 variants you may encounter:
-
Unverified domain —
POST /api/v1/domainsonly accepts domains you have verified (via DNS TXT record, or a matching verified company email domain). The response includeshostnameand averify_urlpointing to the Domain Verification page in your dashboard. -
Scan frequency not allowed — requesting a
scan_frequencyyour tier doesn't support returns the allowed values inallowed_frequenciesplus anupgrade_url. -
Account inactive —
{"error": "Account inactive"}when the account that owns the API key has been deactivated.
404 — Not Found
Returned when a resource doesn't exist or isn't owned by your account — the API never
reveals whether another user's resource exists. Examples:
{"error": "Domain not found"}, {"error": "Scan not found"},
{"error": "API key not found"}.
Requests to an unknown path under /api/ return a generic
{"error": "Not found"}.
405 — Method Not Allowed
Sent when the endpoint exists but doesn't support the HTTP method you used (e.g.
PUT /api/v1/domains/1 instead of PATCH). The response includes an
Allow header listing the permitted methods. Note: 405 responses currently
return a standard HTML error body rather than JSON, so check the status code and
Allow header instead of parsing the body.
409 — Conflict
Returned when creating a resource that already exists — for example, adding a hostname/port combination you're already monitoring:
{
"error": "Domain already exists"
}
429 — Rate Limited
The API enforces two separate limits, and each produces a slightly different 429:
Daily API-key quota
Each API key has a per-day request quota based on your tier (Free 200, Pro 1,000, Business 10,000, Enterprise 100,000 per day). When the quota is exhausted, the response tells you your limit, usage and when the counter resets:
{
"error": "Rate limit exceeded",
"rate_limit": 200,
"requests_today": 200,
"reset_at": "2026-07-15T09:14:02.113841+00:00"
}
The counter resets 24 hours after the previous reset — reset_at is the authoritative
time to wait for.
Per-endpoint burst limits
Some endpoints carry stricter short-window limits per client (for example, on-demand scan triggers
are limited to 20/hour, and /auth/login to 10/minute). These responses have the body
{"error": "Rate limit exceeded"} and include standard rate-limit headers:
HTTP/1.1 429 TOO MANY REQUESTS
Retry-After: 1740
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1752486842
Always respect Retry-After
When present, wait at least that many seconds before retrying. Hammering a rate-limited endpoint
only extends the wait. See Rate Limits
for retry strategies with exponential backoff.
5xx — Server Errors
A 500 means something failed on our side; the body is always
{"error": "Internal server error"} with no further detail. These are logged and
investigated automatically. Treat 5xx responses as transient: retry with exponential
backoff, and contact support@myssl.info if an endpoint
fails persistently.
Handling Errors in Your Code
A robust client distinguishes retryable errors (429, 5xx) from permanent ones (4xx):
import time
import requests
def api_get(url: str, api_key: str, max_retries: int = 3):
headers = {"Authorization": f"Bearer {api_key}"}
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
# Prefer Retry-After; fall back to a short wait
wait = int(response.headers.get("Retry-After", 30))
time.sleep(wait)
continue
if response.status_code >= 500:
time.sleep(2 ** attempt) # transient — exponential backoff
continue
if response.status_code >= 400:
# Permanent client error — don't retry, surface the message
raise RuntimeError(response.json().get("error", "API error"))
return response.json()
raise RuntimeError("Max retries exceeded")