Tools Learn Login Sign up

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 Response
{
  "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.
401 Response
{
  "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:

403 — Tier limit reached
{
  "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 domainPOST /api/v1/domains only accepts domains you have verified (via DNS TXT record, or a matching verified company email domain). The response includes hostname and a verify_url pointing to the Domain Verification page in your dashboard.
  • Scan frequency not allowed — requesting a scan_frequency your tier doesn't support returns the allowed values in allowed_frequencies plus an upgrade_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:

409 Response
{
  "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:

429 — Daily quota
{
  "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:

429 Response 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):

Python
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")

Next Steps

Report a bug

We're new and growing — your feedback helps us improve.

Click to upload, or paste (Ctrl+V) an image