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
| Parameter | Type | Description |
|---|---|---|
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.
| Event | When | Key fields |
|---|---|---|
dns | After DNS backend | status, headline, checks[], detail_url |
tls | After TLS backend | status, headline, checks[], detail_url |
http | After HTTP backend (optional) | status, headline, checks[], status_code, detail_url |
email | After Email backend (optional) | status, headline, checks[], grade, detail_url |
ip | After IP backend | status, headline, checks[], addresses[], detail_url |
summary | After all backends | grade, score, sections, section_grades, hard_fail, not_applicable |
done | Stream complete | domain, duration_ms, cached |
Check status values
Each section event carries a status field:
| Value | Meaning |
|---|---|
pass | All weighted checks pass |
warn | One or more checks have warnings |
fail | One or more checks fail |
error | Backend 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.
| Section | Weight | Hard fail checks |
|---|---|---|
| TLS | 35% | chain_trusted, not_expired |
| DNS | 20% | — |
| HTTP | 20% | — |
| 15% | — | |
| IP | 10% | — |
| Grade | Score |
|---|---|
| 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 Status | Code | Meaning |
|---|---|---|
| 400 | DOMAIN_INVALID | Domain is an IP address, contains wildcards, or exceeds 253 chars |
| 400 | DOMAIN_BLOCKED | Domain resolves to a private/loopback address |
| 400 | INVALID_INPUT | Invalid dkim_selectors (bad characters, too long, too many) |
| 429 | RATE_LIMITED | Rate limit exceeded; check Retry-After header |
| 504 | TIMEOUT | 20-second hard deadline exceeded |