Lens API

Base URL: https://lens.netray.info

Unified domain health check. Runs IP, DNS, TLS, HTTP, and Email checks in parallel and streams results as Server-Sent Events, producing an A+–F grade with per-section breakdowns and hard-fail overrides.

See also: interactive API reference (auto-generated OpenAPI)

Endpoints

GET /api/check/{domain}

Run a full domain health check. Returns an SSE stream by default; append ?stream=false or send Accept: application/json for a single JSON response.

# SSE stream (default)
curl -N https://lens.netray.info/api/check/example.com

# Single JSON response
curl -s https://lens.netray.info/api/check/example.com?stream=false | jq .

# With custom DKIM selectors
curl -N 'https://lens.netray.info/api/check/example.com?dkim_selectors=google,selector1'

POST /api/check

Same check via POST body. Useful when the domain contains characters that are awkward in URLs.

curl -N -X POST https://lens.netray.info/api/check \
  -H 'Content-Type: application/json' \
  -d '{"domain": "example.com"}'

# With DKIM selectors
curl -N -X POST https://lens.netray.info/api/check \
  -H 'Content-Type: application/json' \
  -d '{"domain": "example.com", "dkim_selectors": "google,selector1"}'

GET /api/meta

Returns server version, configured backends, scoring profile, and rate limit settings.

curl -s https://lens.netray.info/api/meta | jq .

Query Parameters

ParameterTypeDescription
stream boolean Set to false for a single JSON response instead of SSE. Default: true.
dkim_selectors string Comma-separated DKIM selectors to test against the email backend. Each selector: characters [a-zA-Z0-9-], 1–63 chars. At most 10 selectors. Example: google,selector1. Absent → beacon uses its built-in provider map.

SSE Event Stream

Results stream as Server-Sent Events in the order each backend completes. IP, DNS, TLS, HTTP, and Email run in parallel; IP waits for DNS-resolved addresses. The stream ends with a summary and a done event.

EventWhenKey fields
dnsAfter DNS backendstatus, headline, checks[], detail_url
tlsAfter TLS backendstatus, headline, checks[], detail_url
httpAfter HTTP backend (optional)status, headline, checks[], status_code, detail_url
emailAfter Email backend (optional)status, headline, checks[], grade, detail_url
ipAfter IP backendstatus, headline, checks[], addresses[], detail_url
summaryAfter all backendsgrade, score, sections, section_grades, hard_fail, not_applicable
doneStream completedomain, duration_ms, cached

Check status values

Each section event carries a status field:

ValueMeaning
passAll weighted checks pass
warnOne or more checks have warnings
failOne or more checks fail
errorBackend unreachable or timed out

Example: dns event

event: dns
data: {
  "status": "warn",
  "headline": "DNSSEC ~  CAA ✓  NS ✓  CNAME-apex ✓",
  "checks": [
    {"name": "dnssec",     "verdict": "warn", "weight": 5, "messages": ["DNSKEY present but no RRSIG found"]},
    {"name": "caa",        "verdict": "pass", "weight": 5},
    {"name": "ns",         "verdict": "pass", "weight": 3},
    {"name": "cname_apex", "verdict": "pass", "weight": 5}
  ],
  "detail_url": "https://dns.netray.info/?q=example.com+%2Bcheck"
}

Example: email event

event: email
data: {
  "status": "pass",
  "headline": "Auth: OK  Infra: OK  Transport: OK  Brand: OK",
  "grade": "A",
  "checks": [
    {"name": "email_authentication", "verdict": "pass", "weight": 10},
    {"name": "email_infrastructure",  "verdict": "pass", "weight": 5},
    {"name": "email_transport",       "verdict": "pass", "weight": 5},
    {"name": "email_brand_policy",    "verdict": "pass", "weight": 2}
  ],
  "detail_url": "https://email.netray.info/?domain=example.com"
}

# For a parked domain (no MX records), infrastructure/transport/brand are skipped:
data: {
  "status": "pass",
  "headline": "Auth: OK  Infra: N/A  Transport: N/A  Brand: N/A",
  "grade": "B",
  "checks": [
    {"name": "email_authentication", "verdict": "pass",  "weight": 10},
    {"name": "email_infrastructure",  "verdict": "skip",  "weight": 5,  "messages": ["No MX records — email receiving not configured"]},
    {"name": "email_transport",       "verdict": "skip",  "weight": 5,  "messages": ["No MX records — email receiving not configured"]},
    {"name": "email_brand_policy",    "verdict": "skip",  "weight": 2,  "messages": ["No MX records — email receiving not configured"]}
  ],
  "detail_url": "https://email.netray.info/?domain=parked.example.com"
}

Example: summary event

event: summary
data: {
  "grade": "A",
  "score": 94.2,
  "overall": "warn",
  "sections": {
    "ip": "pass", "dns": "warn", "tls": "pass", "http": "pass", "email": "pass"
  },
  "section_grades": {
    "ip": "A+", "dns": "B", "tls": "A+", "http": "A", "email": "A"
  },
  "hard_fail": false,
  "hard_fail_checks": [],
  "hard_fail_reason": null,
  "not_applicable": {}
}

not_applicable is always present. When the email backend times out internally it contains {"email": "beacon timeout"} and the email section is excluded from scoring without penalising the overall grade.

Scoring

The overall grade is a weighted average of active section scores. HTTP and Email sections are optional; when not configured they are excluded and the denominator adjusts accordingly.

SectionWeightHard fail checks
TLS35%chain_trusted, not_expired
DNS20%
HTTP20%
Email15%
IP10%
GradeScore
A+≥ 97%
A≥ 90%
B≥ 75%
C≥ 60%
D≥ 40%
F< 40%, or hard-fail override

Caching

Results are cached per domain (default TTL: 5 minutes). The response includes an X-Cache header: HIT for a cached response, MISS otherwise. Requests with dkim_selectors bypass the cache.

Useful One-Liners

# Get the overall grade
curl -s 'https://lens.netray.info/api/check/example.com?stream=false' \
  | jq '.summary.grade'

# CI gate: fail if grade is below A
curl -s 'https://lens.netray.info/api/check/example.com?stream=false' \
  | jq -e '.summary.grade | test("^A")'

# Gate on a specific section
curl -s 'https://lens.netray.info/api/check/example.com?stream=false' \
  | jq -e '.summary.section_grades.tls == "A+"'

# Stream and print each section as it arrives
curl -N https://lens.netray.info/api/check/example.com \
  | grep '^data:' | while read line; do
      echo "${line#data: }" | jq '{section: .status}'
    done

# Check email with custom DKIM selectors
curl -s 'https://lens.netray.info/api/check/example.com?stream=false&dkim_selectors=google,default' \
  | jq '.email.checks'

CI / GitHub Actions

- name: Check domain health
  run: |
    GRADE=$(curl -sf \
      'https://lens.netray.info/api/check/${{ env.DOMAIN }}?stream=false' \
      | jq -r '.summary.grade')
    echo "Grade: $GRADE"
    case "$GRADE" in
      A+|A|B) echo "Health check passed" ;;
      *)      echo "Health check failed: $GRADE"; exit 1 ;;
    esac

Rate Limits

Per-IP GCRA rate limiting: 10 requests per minute, burst of 3. When rate-limited, you receive a 429 response with a Retry-After header.

Errors

{
  "error": {
    "code": "DOMAIN_INVALID",
    "message": "invalid domain: 192.168.1.1"
  }
}
HTTP StatusCodeMeaning
400DOMAIN_INVALIDDomain is an IP address, contains wildcards, or exceeds 253 chars
400DOMAIN_BLOCKEDDomain resolves to a private/loopback address
400INVALID_INPUTInvalid dkim_selectors (bad characters, too long, too many)
429RATE_LIMITEDRate limit exceeded; check Retry-After header
504TIMEOUT20-second hard deadline exceeded