FortifyNet API

Read your organizations, domains, audit reports, uptime and alerts from your own systems, start audits, and receive events as webhooks.

API access is included in Professional and Enterprise. Webhooks and the IP allowlist for keys are included in Enterprise.

Getting started

  1. Create an API key under Settings, API and webhooks. The key is shown once.
  2. Send the key in the X-API-Key header with every call. Never put it in the address.
  3. Answers are JSON: {"data": ...} when it works, {"error": {"code", "message"}} when it does not. Times are UTC in ISO 8601 form.
  4. The API is meant for servers. It cannot be called from web pages on other sites, which keeps keys out of browsers.
curl
curl -H "X-API-Key: $FORTIFYNET_KEY" \
  "https://fortifynet.com/functions/api-v1-domains?limit=50"
200 OK
{
  "data": [
    {
      "id": "665f1c2a9b3e4d5f6a7b8c9d",
      "organization_id": "665f1b0e9b3e4d5f6a7b8c11",
      "domain": "example.com",
      "verified": true,
      "tags": ["customer-a"],
      "scores": { "overall": 84, "ssl": 100, "dns": 67, "performance": 95, "security_headers": 48, "dark_web": 95 },
      "last_audit_at": "2026-11-12T14:19:39.135Z",
      "monitoring": { "enabled": true, "running": true, "interval_hours": 24, "paused": false },
      "uptime": { "enabled": true, "interval_minutes": 60, "is_down": false, "last_checked_at": "2026-11-12T18:46:46.971Z" }
    }
  ],
  "meta": { "count": 1, "has_more": false, "next_offset": null }
}

What a key reaches

A key acts as your account. It reaches the organizations you belong to, with your role there: reading works with any role, starting an audit needs owner or admin. The API is open for an organization when its owner is on Professional or Enterprise. A key can be narrowed to one organization, to reading only, to certain IP addresses, and given an end date.

Endpoints

MethodAddressWhat it does
GET/api-v1-meThe key in use, its limits and the address your call comes from. A good first test.
GET/api-v1-organizationsThe organizations the key reaches, with your role and whether the API is open there.
GET/api-v1-domainsDomains with scores, monitoring and uptime state. One domain with "id".
GET/api-v1-reportsAudit reports, newest first. One report with checks and findings with "id".
GET/api-v1-uptimeAvailability, outages and stored checks of one domain, for up to 90 days.
GET/api-v1-alertsMonitoring alerts, newest first.
POST/api-v1-auditsRuns a full audit of a domain and returns the finished report. Normally answers within a minute.
GET/api-v1-openapiThis API as an OpenAPI 3.0 document. Needs no key.

https://fortifynet.com/functions/api-v1-…

Parameters

Parameter names, values and error codes are the same in every language.

api-v1-domains

idReturn this domain only (as an object in data).
organization_idOnly domains of this organization.
domainOnly the domain with exactly this name, for example example.com.
tagOnly domains carrying this tag.
limit, offsetPaging. limit 1 to 500 (default 100). meta.next_offset is the next page.

api-v1-reports

idReturn this report with its checks and findings.
domain_idOnly reports of this domain.
organization_idOnly reports of this organization.
statuspending, running, completed or failed.
limit, beforePaging. limit 1 to 100 (default 25). Pass meta.next_before as before for the next page.

api-v1-uptime

domain_idRequired. The domain.
hoursLength of the window, 1 to 2160 (default 24).

api-v1-alerts

domain_id, organization_idOnly alerts of this domain or organization.
since, beforeOnly alerts created at or after since, and before before (ISO 8601).
limit1 to 200 (default 50).

api-v1-audits (JSON body)

domain_idRequired. The domain to audit. The key needs the audit permission, and your role must be owner or admin.
langLanguage of the findings: en, sv, de, es or fi (default en).
curl
curl -X POST -H "X-API-Key: $FORTIFYNET_KEY" -H "Content-Type: application/json" \
  -d '{"domain_id": "665f1c2a9b3e4d5f6a7b8c9d"}' \
  "https://fortifynet.com/functions/api-v1-audits"

Errors

An error answer carries a code that does not change and a message for people.

401missing_api_key, invalid_api_key, api_key_revoked, api_key_expiredNo key, a wrong key, or a key that is no longer valid.
403plan_requiredThe owner of the organization is on a plan without API access.
403ip_not_allowedThe key has an IP allowlist and the call came from another address.
403missing_scope, forbiddenThe key is read only, or your role in the organization is viewer.
400invalid_parameter, invalid_bodyA parameter or the body is not valid. error.parameter names it.
404not_foundNo such domain, report or organization, or the key does not reach it.
405method_not_allowedWrong method. The Allow header names the right one.
422domain_not_foundThe domain name does not exist in DNS, so it cannot be audited.
429rate_limitedToo many requests. Wait the number of seconds in Retry-After.
502audit_failedThe audit could not be completed. error.report_id names the failed report.
500server_errorSomething went wrong on our side.

Limits

120 requests a minute per key. 30 audits an hour per key and 300 a day per account. When a limit is reached the answer is 429 and the Retry-After header says how many seconds to wait. Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

Webhooks

A webhook sends an event to an https address of yours the moment it happens, as a POST with a JSON body. Webhooks are set up under Settings, API and webhooks, and cover the organizations you own.

Events

audit.completedAudit finisheddata: report_id, trigger (scheduled, manual or api), overall_score, previous_overall_score, finding_counts, tests_run, report_url
uptime.downSite downdata: error, status_code, checked_at
uptime.upSite up againdata: down_since, downtime_seconds, status_code, response_time_ms, checked_at
monitoring.pausedMonitoring pauseddata: reason (unreachable, never_reachable, dns or tls), unreachable_since, last_error
monitoring.resumedMonitoring resumeddata: paused_since, status_code

What is sent

HTTP
POST /your/address
Content-Type: application/json
X-FortifyNet-Event: uptime.down
X-FortifyNet-Delivery: 665f2d3a9b3e4d5f6a7b8c22
X-FortifyNet-Timestamp: 1794513301
X-FortifyNet-Signature: v1=5f2c...

{
  "id": "evt_3b1f0c9a7d2e4f6a8b0c1d2e3f4a5b6c",
  "type": "uptime.down",
  "created_at": "2026-11-12T19:55:01.000Z",
  "api_version": "1",
  "organization": { "id": "665f1b0e9b3e4d5f6a7b8c11", "name": "Customer A" },
  "domain": { "id": "665f1c2a9b3e4d5f6a7b8c9d", "name": "example.com" },
  "data": { "error": "HTTP 503", "status_code": 503, "checked_at": "2026-11-12T19:55:00.000Z" }
}

Check that it comes from us

  • Every request carries X-FortifyNet-Timestamp (seconds) and X-FortifyNet-Signature.
  • The signature is "v1=" followed by the HMAC-SHA256, in hex, of the timestamp, a full stop and the raw body, with the webhook's signing secret as key.
  • Compute the same value and compare. Refuse requests whose timestamp is more than five minutes old, and answer 2xx within 8 seconds.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as it arrived (a string), before any JSON parsing
export function isFromFortifyNet(rawBody, headers, secret) {
  const timestamp = headers["x-fortifynet-timestamp"];
  const signature = headers["x-fortifynet-signature"] || "";
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = "v1=" + createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}
Python
import hashlib, hmac, time

def is_from_fortifynet(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("X-FortifyNet-Timestamp", "")
    signature = headers.get("X-FortifyNet-Signature", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

When your server does not answer

A failed delivery is tried again after 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. After 15 failures in a row the webhook is switched off and you get a mail.

AI assistants (MCP)

Claude, ChatGPT and other clients that speak the Model Context Protocol can use the same data as the API on behalf of a person who signs in: domains, reports, uptime and alerts, and starting audits. No API key is used: the person signs in to FortifyNet and approves the connection. The address is https://fortifynet.com/api/mcp. Instructions and limits are under Settings, AI assistant.

An audit started by an assistant is reported with trigger "api" in the audit.completed event.

OpenAPI

The whole API as an OpenAPI 3.0 document, for Swagger UI, Postman and code generators:

https://fortifynet.com/functions/api-v1-openapi