API Enterno.io
Programmatic access to all enterno.io tools: HTTP headers, DNS, SSL, ping, IP geolocation, health scores, monitors and status pages. REST API with JSON responses.
Open API — no key, no signup
One GET, plain text back. Paste it into a terminal, a browser, a CI script or a spreadsheet and it answers — nothing to read first, nothing to sign up for. The keyed API below does more; this is the part you can try in the next five seconds.
curl "https://enterno.io/api/open/headers?q=https://example.com"
curl "https://enterno.io/api/open/ssl?q=example.com"
curl "https://enterno.io/api/open/dns?q=example.com&types=MX,TXT"
/api/open/ with no tool prints the catalogue, so you can find your way around from the command line without coming back here.
Tools
| Tool | q = | Description | Extra |
|---|---|---|---|
headers |
url |
HTTP response headers | — |
dns |
domain |
DNS records | types |
ssl |
host |
TLS certificate | port |
ip |
host |
IP geolocation | — |
ping |
host |
ICMP reachability | — |
whois |
host |
Domain registration | — |
mx |
domain |
Mail exchangers | — |
asn |
query |
ASN / network owner | — |
dkim |
domain |
DKIM public key | selector |
propagation |
domain |
DNS propagation across resolvers | — |
http-status |
url |
Status code only | — |
redirects |
url |
Redirect chain | max |
csp |
url |
Content-Security-Policy | — |
cors |
url |
CORS policy | origin |
cookie |
url |
Cookie flags | — |
robots |
domain |
robots.txt rules | test_url |
schema |
url |
Structured data | — |
protocol |
url |
HTTP version and TLS | — |
url-expand |
url |
Resolve a short link | — |
Formats
&format=text (default) for a shell, &format=json for code, &format=csv for a spreadsheet. Text output ends with a # comment naming the source, so it survives being piped through grep and still says where it came from when someone pastes it into an issue.
Limits, and what is not here
Rate-limited per IP and metered by the same daily allowance the site uses for anonymous visitors. Answers are cached for 60 seconds and readable from any origin.
Two families of tools stay behind a key. Anything with a real bill attached — headless browser, site crawl, paid third-party lookups — because a keyless caller can repeat it without limit and without a name to bill. And anything that probes a host the way its owner would read as a scan: port sweeps, subdomain enumeration, malware and abuse lookups. Keyless, those are not diagnostics; they are a way to scan somebody else from our address. ping is here as reachability only, never as its port-scan mode.
Need a tool that is not on the list, or the same call without the anonymous ceiling? That is what the keyed API below is for.
What to build with it
Each of these is a whole use of the API, not a demo of one endpoint.
1. Fail a build before the certificate does
A certificate that expires on a Saturday is discovered by customers. Put the check in CI and it is discovered by a pipeline instead.
# .gitlab-ci.yml / GitHub Actions — no key needed
DAYS=$(curl -s "https://enterno.io/api/open/ssl?q=$SITE&format=json" | jq -r '.certificate.days_left')
[ "$DAYS" -gt 14 ] || { echo "TLS expires in $DAYS days"; exit 1; }
2. A spreadsheet that checks itself
Google Sheets reads CSV over HTTP natively. One formula per row, refreshed on open — no script, no add-on, no key.
=IMPORTDATA("https://enterno.io/api/open/ssl?q=" & A2 & "&format=csv")
3. Uptime from your own cron
When you would rather own the schedule and the alerting, and only borrow the check.
*/5 * * * * curl -sf "https://enterno.io/api/open/http-status?q=https://example.com" \
| grep -q "^code: 200" || /usr/local/bin/notify "example.com is not answering 200"
4. Give an AI agent a tool it can actually call
An agent cannot sign up for an API key. A keyless GET that returns plain text is the shape a tool-using model handles without a wrapper — and for a full, typed tool list there is an MCP server.
5. Zapier, Make, n8n
Every one of them has a "GET a URL" step and struggles with anything else. &format=json drops straight into the next node.
6. One-liners worth keeping
# which CDN is in front of it
curl -s "https://enterno.io/api/open/headers?q=https://example.com" | grep -i "server\|cf-ray"
# where does this short link really go
curl -s "https://enterno.io/api/open/url-expand?q=https://bit.ly/xxxx"
# is my SPF/DKIM actually published
curl -s "https://enterno.io/api/open/dkim?q=example.com&selector=default"
7. Embed a live result in someone else's page
The open tier is readable from any origin, so a status widget can be a few lines of fetch() with no proxy of your own. For a picture rather than data there are badges.
8. Auditing a list
For a handful of hosts, a shell loop and the open tier are enough. Past that the anonymous ceiling is the wrong tool — use a key and /api/v4/batch, which runs the list asynchronously instead of making you pace it yourself.
while read h; do
printf '%s\t' "$h"
curl -s "https://enterno.io/api/open/ssl?q=$h" | grep '^certificate.days_left:'
done < hosts.txt
Try it
Fire a request against your key and copy a ready-made snippet in one of 5 languages.
GET /api/v4/check — HTTP headers check
GET /api/v4/dns — DNS lookup
GET /api/v4/ssl — SSL/TLS certificate
GET /api/v4/ping — Ping + ports
GET /api/v4/ip — IP geolocation
Authentication
All API v4 requests require a key via X-API-Key header. Use X-Idempotency-Key to safely retry write requests.
Get an API key by creating an account or in your dashboard.
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/webhooks"
Every response includes an X-Request-Id header for tracing.
Idempotency Key
Add X-Idempotency-Key: unique-id to any write request. Enterno.io returns the cached response for duplicate requests within 60 seconds.
curl -X POST "https://enterno.io/api/v4/webhooks" \
-H "X-API-Key: YOUR_KEY" \
-H "X-Idempotency-Key: create-wh-001" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.com/hook"}'
Response Format
API v4 uses the same JSON envelope as v3 with "api_version": "4.0" in meta.
{
"data": { ... },
"meta": {
"request_id": "a1b2c3d4e5f6a7b8c9d0e1f2",
"duration_ms": 8,
"cached": false,
"api_version": "4.0"
}
}
On error:
{
"error": {
"code": "not_found",
"message": "Resource not found"
},
"meta": {
"request_id": "a1b2c3d4e5f6a7b8c9d0e1f2",
"duration_ms": 2,
"api_version": "4.0"
}
}
Rate Limits
Rate limit information is included in response headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute |
X-RateLimit-Remaining | Remaining requests in current window |
X-RateLimit-Reset | Unix timestamp when rate limit resets |
Retry-After | Seconds to wait (only on 429) |
| Plan | Requests/min | Daily Limit | Scopes |
|---|---|---|---|
| Free | 10 | 100 | check, dns |
| Pro | 60 | 5 000 | check, dns, ssl, ip, ping, monitors, webhook |
| Business | 120 | 50 000 | check, dns, ssl, ip, ping, monitors, webhook |
Scopes
Each API key has a set of permitted scopes. A request to an inaccessible endpoint will return a 403 error.
| Scope | Endpoint | Description |
|---|---|---|
check | /api/v4/check, /api/v4/health, /api/v4/audit-export | HTTP header check + health score |
dns | /api/v4/dns | DNS lookup + DNSSEC |
ssl | /api/v4/ssl | SSL/TLS check + chain details |
ip | /api/v4/ip | IP geolocation |
ping | /api/v4/ping | Ping, port check, traceroute |
monitors | /api/v4/monitors | Monitor CRUD + status pages |
webhook | /api/v4/webhooks, /api/v4/events | Webhook subscription and event log CRUD |
Webhooks — Manage Subscriptions
Create webhook subscriptions to receive HTTP POST notifications when monitors, SSL certificates, or domains change state.
Supported event types
| Scope | Description |
|---|---|
monitor.down | Monitor went down |
monitor.up | Monitor recovered |
monitor.degraded | Monitor response degraded |
ssl.expiring | SSL certificate expiring soon |
ssl.expired | SSL certificate expired |
ssl.chain_changed | SSL chain changed (issuer or fingerprint) |
domain.expiring | Domain expiring soon |
domain.expired | Domain registration has lapsed |
visual.changed | Visual monitor spotted a change |
test | Manual test event |
Webhook limits by plan
| Plan | Max webhooks | Event restrictions |
|---|---|---|
| Free | 1 | monitor.down, monitor.up, test |
| Starter | 5 | All events |
| Pro | 20 | All events |
| Business | 50 | All events |
List webhooks
Returns all webhook subscriptions for the authenticated user.
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/webhooks"
Create webhook
| Name | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Destination URL (HTTPS required) |
name | string | No | Subscription name (optional) |
secret | string | No | HMAC-SHA256 secret for payload signature (optional) |
events | array | No | Event types to subscribe to (default: all) |
curl -X POST "https://enterno.io/api/v4/webhooks" \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/enterno",
"name": "Production alerts",
"events": ["monitor.down", "monitor.up", "ssl.expiring"]
}'
Response
{
"data": {
"id": 12,
"name": "Production alerts",
"url": "https://your-app.com/hooks/enterno",
"secret": "ent_wh_sk_***",
"events": ["monitor.down", "monitor.up", "ssl.expiring"],
"is_active": true,
"created_at": "2026-03-28T06:00:00Z"
},
"meta": {"request_id": "...", "duration_ms": 18, "api_version": "4.0"}
}
Update webhook
curl -X PUT "https://enterno.io/api/v4/webhooks?id=12" \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'
Delete webhook
curl -X DELETE "https://enterno.io/api/v4/webhooks?id=12" \
-H "X-API-Key: YOUR_KEY"
Verifying webhook signatures
Every delivery to your webhook URL is signed with your shared secret. Two headers enable request-integrity + replay-protection checks:
| Header | Value | Purpose |
|---|---|---|
X-Enterno-Timestamp |
Unix timestamp (seconds) at dispatch | Reject if > 300 s skew from receiver clock |
X-Enterno-Signature-V2 |
sha256=<hex> |
HMAC-SHA256(secret, timestamp + "." + body) |
X-Enterno-Signature |
sha256=<hex> |
Legacy — HMAC over body only, no replay protection. Kept for existing integrations. Will be removed 2026-07-01. |
PHP verifier (recommended)
<?php
$secret = getenv('ENTERNO_WEBHOOK_SECRET'); // your shared secret
$ts = $_SERVER['HTTP_X_ENTERNO_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_ENTERNO_SIGNATURE_V2'] ?? '';
$body = file_get_contents('php://input');
// 1. Replay window — reject timestamps older than 5 minutes
if (abs(time() - (int)$ts) > 300) {
http_response_code(400);
exit('stale timestamp');
}
// 2. Compute expected signature
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
// 3. Timing-safe compare
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('bad signature');
}
$payload = json_decode($body, true);
// …handle the event…
Node.js verifier
const crypto = require('crypto');
function verify(req, secret) {
const ts = req.headers['x-enterno-timestamp'];
const sig = req.headers['x-enterno-signature-v2'];
const body = req.rawBody; // use a raw-body parser
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300)
throw new Error('stale timestamp');
const expected =
'sha256=' + crypto.createHmac('sha256', secret)
.update(ts + '.' + body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)))
throw new Error('bad signature');
}
Migration window: legacy X-Enterno-Signature (body-only, no timestamp) is still sent alongside V2 until 2026-07-01. If you still depend on it, migrate to V2 before that date.
Send test event
curl -X POST "https://enterno.io/api/v4/webhooks?action=test&id=12" \
-H "X-API-Key: YOUR_KEY"
Events — Delivery History
View webhook delivery attempts, retry counts, response codes, and failure reasons.
| Name | Type | Description |
|---|---|---|
webhook_id | integer | Filter by webhook ID (optional) |
event_type | string | Filter by event type |
status | string | Filter: delivered, retrying, failed, dead, pending |
page | integer | Page number (default 1) |
per_page | integer | Items per page, max 100 (default 25) |
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/events?webhook_id=12&status=failed"
Response
{
"data": [
{
"id": 501,
"webhook_id": 12,
"event_type": "monitor.down",
"status": "delivered",
"http_status_code": 200,
"attempts": 1,
"created_at": "2026-03-28T06:05:00Z",
"delivered_at": "2026-03-28T06:05:01Z"
}
],
"meta": {
"request_id": "...",
"duration_ms": 12,
"api_version": "4.0",
"pagination": {"total": 1, "page": 1, "per_page": 25, "pages": 1}
}
}
Check — HTTP Header Check
Get HTTP response headers with detailed timing breakdown (DNS, connect, TLS, TTFB).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL to check |
method | string | No | GET, HEAD, POST (default: GET) |
follow | string | No | 0/1 (default: 1) |
timeout | integer | No | 1-30 (default: 15) |
ua | string | No | Custom User-Agent |
Example
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/check.php?url=https://example.com"
Response
{
"data": {
"url": "https://example.com",
"final_url": "https://example.com/",
"method": "GET",
"code": 200,
"http_version": "HTTP/2",
"ip": "93.184.216.34",
"headers": [...],
"timing": {"dns_ms":12,"connect_ms":45,"tls_ms":78,"ttfb_ms":120,"transfer_ms":142,"redirect_ms":0,"redirect_count":0},
"elapsed_ms": 142
},
"meta": {"request_id":"req_01...","duration_ms":150,"cached":false,"api_version":"4.0"}
}
DNS — DNS Lookup
Get DNS records with optional DNSSEC validation.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Domain name |
types | string | No | Comma-separated: A,AAAA,MX,NS,TXT,CNAME,SOA |
dnssec | string | No | 1 to check DNSSEC status |
Example
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/dns.php?domain=example.com&dnssec=1"
SSL — SSL/TLS Check
Check SSL certificate with detailed chain information.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
host | string | Yes | Hostname |
port | integer | No | Port (default 443) |
Example
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/ssl.php?host=example.com"
IP — Geolocation
Determine location, ISP and organization by IP address or domain.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
host | string | Yes | IP address or domain |
Example
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/ip.php?host=8.8.8.8"
Ping — Ping, Ports & Traceroute
Ping a host, check ports, or run a traceroute.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
host | string | Yes | Host or IP |
action | string | No | ping, ports, traceroute (default: ping) |
count | integer | No | Ping count 1-10 (default 4) |
ports | string | No | Comma-separated ports (for action=ports) |
max_hops | integer | No | Max hops 1-30 (for action=traceroute) |
Example
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/ping.php?host=example.com&action=traceroute"
Health — Website Health Score
Comprehensive website health analysis: security headers (30pts), SSL/TLS (25pts), performance (25pts), best practices (20pts). Returns a score 0-100 with grade A+ to F.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL to analyze |
Example
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/health.php?url=example.com"
Returns score 0–100, letter grade A+–F, Security Headers / SSL / Performance / Best Practices category breakdown, and prioritised recommendations. Cached 2 minutes per URL.
Monitors — CRUD
Create, read, update and delete uptime monitors via API.
List monitors
| Name | Type | Description |
|---|---|---|
page | integer | Page number (default 1) |
per_page | integer | Items per page, max 100 (default 25) |
status | string | Filter: up, down, unknown |
Get single monitor
Returns monitor details with 20 most recent checks.
Create monitor
curl -X POST "https://enterno.io/api/v4/monitors.php" \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: create-mon-example-001" \
-d '{
"url": "https://example.com",
"check_type": "http",
"interval_minutes": 5,
"expected_code": 200,
"notify_email": true
}'
Update monitor
curl -X PUT "https://enterno.io/api/v4/monitors.php?id=5" \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"interval_minutes": 10, "is_active": false}'
Delete monitor
curl -X DELETE "https://enterno.io/api/v4/monitors.php?id=5" \
-H "X-API-Key: YOUR_KEY"
Batch — async URL checker
Submit up to 100 URLs in one call (plan-dependent: free→5, starter→20, pro→50, business→100). Returns a job_id immediately; poll for results.
Submit a batch
curl -X POST "https://enterno.io/api/v4/batch.php" \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://example.com", "https://example.org"],
"type": "check"
}'
{
"data": {
"job_id": "bq_01ARZ3...",
"status": "pending",
"type": "check",
"total": 2,
"poll_url": "/api/v4/batch.php?job_id=bq_01ARZ3...",
"invalid_urls": []
},
"meta": {"request_id":"req_...","duration_ms":8,"api_version":"4.0"}
}
Poll status
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/batch.php?job_id=bq_01ARZ3..."
Status — Public Status Page
Get status page data as JSON. No authentication required when slug is provided.
Example
curl "https://enterno.io/api/v4/status.php?slug=my-company"
Response
{
"data": {
"title": "My Company Status",
"slug": "my-company",
"overall_status": "operational",
"monitors_total": 5,
"monitors_down": 0,
"monitors": [
{"id": 1, "name": "example.com", "status": "up", "response_time_ms": 120}
],
"recent_incidents": []
},
"meta": {"request_id":"req_...","duration_ms":15,"cached":false,"api_version":"4.0"}
}
Every other tool
The endpoints above are the ones with hand-written handlers. Every other diagnostic tool on the site answers at /api/v4/<tool> under the same auth, the same envelope and the same rate limits — it runs the identical code the site runs, so a result here and a result on the tool page cannot disagree.
curl -H "X-API-Key: YOUR_KEY" \
"https://enterno.io/api/v4/cookie?url=https://example.com"
# what this key may call, and what it may not
curl -H "X-API-Key: YOUR_KEY" "https://enterno.io/api/v4/tools"
/api/v4/tools answers for the key that asked: each row carries an allowed flag from that key's scopes, so a client building a menu knows which rows would 403 before it offers them.
Tools and scopes
| Endpoint | Parameter | Scope | Description |
|---|---|---|---|
/api/v4/cookie |
url |
check |
Cookie flags and trackers |
/api/v4/cors |
url + origin |
check |
CORS policy |
/api/v4/csp |
url |
check |
Content-Security-Policy grade |
/api/v4/http-status |
url |
check |
Status code and timing |
/api/v4/redirects |
url + max, ua |
check |
Redirect chain |
/api/v4/protocol |
url |
check |
HTTP version and TLS negotiation |
/api/v4/mixed-content |
url |
check |
Mixed content on an HTTPS page |
/api/v4/schema |
url |
check |
Structured data |
/api/v4/robots |
domain + test_url |
check |
robots.txt rules |
/api/v4/tech-detect |
url |
check |
Technology fingerprint |
/api/v4/og-preview |
url |
check |
Open Graph and social preview |
/api/v4/cms |
url |
check |
CMS detection |
/api/v4/url-expand |
url |
check |
Resolve a short link |
/api/v4/resolve-url |
url |
check |
Resolve a URL to its final target |
/api/v4/wayback |
url |
check |
Wayback Machine history |
/api/v4/security |
url |
check |
Security headers grade |
/api/v4/seo-audit |
url |
check |
On-page SEO audit |
/api/v4/ai-check |
domain |
check |
AI readiness |
/api/v4/carbon |
url |
check |
Carbon footprint estimate |
/api/v4/performance |
url |
check |
Performance metrics |
/api/v4/pagespeed |
url + strategy |
check |
PageSpeed |
/api/v4/mx |
domain |
dns |
Mail exchangers |
/api/v4/dkim |
domain + selector |
dns |
DKIM public key |
/api/v4/email-check |
domain |
dns |
SPF, DKIM and DMARC |
/api/v4/email-header |
headers |
dns |
Parse raw email headers |
/api/v4/propagation |
domain + type |
dns |
DNS propagation across resolvers |
/api/v4/smtp-test |
target + port |
dns |
SMTP banner and STARTTLS |
/api/v4/whois |
host |
dns |
Domain registration |
/api/v4/asn |
query |
ip |
ASN and network owner |
/api/v4/reverse-ip |
ip |
ip |
Domains sharing an address |
/api/v4/traceroute |
host |
ping |
Network route |
Scopes are the ones keys already carry, not one per tool: a key issued for check can call the cookie and CSP tools without being reissued. The mapping follows what a tool reads rather than what it reports — the CSP grader fetches a page over HTTP, so it is check, however security-shaped its output looks.
Port sweeps, subdomain enumeration and malware lookups are not here. A key names who is calling; it does not say they own the host being probed, and that is the permission that matters. Those capabilities live in the products that gate on proven ownership.
MCP — for AI agents
The same tools again, this time over the Model Context Protocol, so an agent in Claude Desktop, Cursor or Zed can call them without anyone writing HTTP code. It is the same handler underneath as the REST endpoints above and the tool pages on the site — three ways in, one implementation, so the three cannot disagree about what a check returns.
{"jsonrpc":"2.0","id":1,"method":"tools/list"}
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"check_ssl","arguments":{"host":"example.com"}}}
43 tools are exposed — every REST tool above plus the platform queries. tools/list returns the current set with argument schemas; ready-made config snippets for each agent are on the MCP server page.
An agent with no key at all is not stuck: /api/open/ above needs none, answers plain text, and is listed in our agent card as the skill open-tools.
Audit export (signed)
GET /api/v4/audit-export — returns an integrity-signed (HMAC-SHA256) compliance snapshot of a monitor, a shared check, or your whole tenant. Scope check. Captain-tier.
| Name | Type | Required | Description |
|---|---|---|---|
type | string | yes | monitor | check | tenant |
id | string | for monitor/check | monitor id (int) or shared-check id; omit for tenant |
page, per_page | int | no | pagination, tenant only (per_page ≤ 200) |
Response — the signed envelope:
{
"data": {
"data": { /* canonical payload */ },
"sig": { "v":1, "alg":"HMAC-SHA256", "kid":"e1", "ts":1718000000, "value":"<hex>" }
},
"meta": { "request_id":"req_...", "api_version":"4.0" }
}
Verify by re-canonicalising data and re-HMACing ${ts}.${canonical} against the shared secret. Reference verifier: cli/verify-report.php.
Error Codes
| HTTP Code | Error Code | Description |
|---|---|---|
400 | missing_parameter | Missing or invalid parameters |
401 | auth_required | Missing API key |
401 | invalid_api_key | Invalid or inactive API key |
403 | insufficient_scope | Key does not have the required scope |
404 | not_found | Resource not found |
405 | method_not_allowed | HTTP method not allowed |
409 | conflict | Idempotent request already in progress |
429 | rate_limit_exceeded | Rate limit exceeded |
429 | daily_limit_exceeded | Daily API request limit exceeded |
Migrating from earlier versions
See the full step-by-step guide at /docs/api-v4-migration.md. Quick map below.
| From | To v4 | Description |
|---|---|---|
/api/v1/{check,dns,ssl,ip,ping}.php | /api/v4/{check,dns,ssl,ip,ping}.php | Flat JSON → {data,meta} envelope. Auth header unchanged. |
/api/v2/{check,dns,ssl,ip,ping}.php | /api/v4/{check,dns,ssl,ip,ping}.php | {ok,data,meta} → {data,meta}. Error shape changes to {error:{code,message}}. |
/api/v3/* | /api/v4/* | Identical contract — just swap /v3/ → /v4/. |
| Webhook config (UI only) | /api/v4/webhooks | Programmatic webhook CRUD — v4-only. |
| Delivery log (UI only) | /api/v4/events | Webhook delivery history with pagination and filters. |
Timeline: v1 and v2 return 410 Gone after 2026-10-01. v3 remains on a freeze (no new features), no sunset date set — migrate opportunistically. Every v1/v2 response already carries Deprecation + Sunset headers (F2.2 / M-08).
Heartbeat / dead-man's switch
Per-monitor token endpoint for cron jobs and scheduled tasks to ping us regularly. If no ping arrives in the expected interval, the monitor flips to down and alerts fire — same pathway as HTTP/SSL/DNS monitors. No API key required; the token in the URL is the auth.
The token is generated when you create a heartbeat monitor in the dashboard. Put the URL into your cron job:
# every 10 minutes
*/10 * * * * curl -fsS https://enterno.io/api/heartbeat/YOUR_TOKEN > /dev/null
Response
{"status":"ok","next_ping_expected":1745197200}
Rate-limited to 30 req/min/IP via getClientIp(). Returns {"status":"ok", ...} for valid + active tokens, same response shape for inactive/unknown tokens (enumeration-safe).
Webhook receivers — verifying our signatures
When enterno.io dispatches a webhook (monitor.down, ssl.expiring, etc.), every POST carries three headers to let you verify authenticity and replay-protect:
| Header | Description |
|---|---|
X-Enterno-Event | Event type: monitor.down, monitor.up, monitor.degraded, ssl.expiring, ssl.expired, ssl.chain_changed, domain.expiring, domain.expired, visual.changed, test. |
X-Enterno-Timestamp | Unix timestamp of dispatch. Reject if abs(now - ts) > 300s (replay guard). |
X-Enterno-Signature-V2 | HMAC-SHA256 over "{timestamp}.{raw_body}". Prefix sha256=. |
X-Enterno-Signature (legacy) | HMAC-SHA256 body-only — sunset 2026-07-01. Migrate to V2 now. |
PHP
$raw = file_get_contents(\'php://input\');
$ts = (int) ($_SERVER[\'HTTP_X_ENTERNO_TIMESTAMP\'] ?? 0);
$sig = $_SERVER[\'HTTP_X_ENTERNO_SIGNATURE_V2\'] ?? \'\';
// 1. replay guard
if (abs(time() - $ts) > 300) { http_response_code(401); exit(\'stale\'); }
// 2. verify signature (timing-safe)
$expected = \'sha256=\' . hash_hmac(\'sha256\', $ts . \'.\' . $raw, $webhookSecret);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit(\'bad sig\'); }
// 3. process event
$event = json_decode($raw, true);
// ... your logic
http_response_code(200);
Node.js
const crypto = require(\'crypto\');
const secret = process.env.ENTERNO_WEBHOOK_SECRET;
app.post(\'/webhook\', express.raw({ type: \'application/json\' }), (req, res) => {
const ts = parseInt(req.get(\'X-Enterno-Timestamp\') || \'0\', 10);
const sig = req.get(\'X-Enterno-Signature-V2\') || \'\';
if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(401).send(\'stale\');
const expected = \'sha256=\' + crypto.createHmac(\'sha256\', secret)
.update(ts + \'.\' + req.body.toString())
.digest(\'hex\');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send(\'bad sig\');
}
const event = JSON.parse(req.body.toString());
// ... your logic
res.status(200).send();
});