CertIQ
API · v1 · beta

Embed CertIQ scoring into your quote engine.

A simple REST API: post a domain, receive a combined score, per-signal breakdown, and a branded PDF. Built for broker platforms and MGAs embedding CertIQ's risk signal into their existing workflow.

Overview

The CertIQ API scores any domain's externally observable cyber posture in under a minute. Typical integration: an insurer or broker captures a prospect's website at quote intake, calls POST /api/v1/scans, and receives a 202 with a scan token. The scan runs async (~30s); the partner polls or receives a webhook when it completes, then fetches the branded PDF for the underwriting file.

The base URL for v1 is:

https://certiq.au/api/v1

All requests and responses are JSON unless otherwise noted. All timestamps are ISO-8601 in UTC. All scan_token values are opaque strings — treat them as case-sensitive identifiers.

Authentication

Every request requires a Bearer token in the Authorization header. You can view and rotate your key in the partner dashboard.

curl https://certiq.au/api/v1/scans \
  -H "Authorization: Bearer YOUR_API_KEY"

Requests without a valid, active key return 401 Unauthorised. Rotating a key invalidates the previous one immediately — coordinate rollouts with your integration.

Create a scan

POST /api/v1/scans

Starts a new scan for the given domain. Returns 202 Accepted immediately with a scan token. The scan runs asynchronously; poll GET /api/v1/scans/:token or subscribe to the scan.completed webhook.

Request body

Field Type Description
domain string Apex domain to scan (e.g. example.com.au). Protocol and path are stripped.

Example

curl -X POST https://certiq.au/api/v1/scans \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com.au"}'

Response — 202 Accepted

{
  "scan_token": "a1b2c3d4e5f6…",
  "status": "pending",
  "status_url": "https://certiq.au/api/v1/scans/a1b2c3d4e5f6…",
  "self_assessment_url": "https://certiq.au/scans/a1b2c3d4e5f6…",
  "estimated_seconds": 30
}

self_assessment_url is a public, login-free page the insured can use to complete the Essential Eight self-assessment — the scan_token in the URL is the only authentication required. Forward this URL to the insured via email (or embed in your own portal). When they finish, CertIQ fires the scan.self_assessment_completed webhook and combined_score becomes available on the retrieve-scan endpoint.

Returns 422 Unprocessable Entity with {"error":"Invalid domain"} if the domain fails validation.

Retrieve a scan

GET /api/v1/scans/:token

Returns the current state of a scan. While the scan is running, only scan_token, domain, status, and partial signals are populated. Once status == "complete", the full scoring payload is included.

Status values

  • pending — queued, not yet running
  • scanning — signals in progress
  • scoring — signals done, finalising score
  • complete — done; full payload available
  • failed — scan aborted; try again

Response — 200 OK (complete)

{
  "scan_token": "a1b2c3d4e5f6…",
  "domain": "example.com.au",
  "status": "complete",
  "signals": {
    "scan":       { "status": "complete", "score": 22, "max_score": 25, "findings": [...] },
    "breach":     { "status": "complete", "score": 18, "max_score": 20, "findings": [...] },
    "reputation": { "status": "complete", "score": 20, "max_score": 20, "findings": [] },
    "mail":       { "status": "complete", "score": 16, "max_score": 20, "findings": [...] },
    "layer":      { "status": "complete", "score": 13, "max_score": 15, "findings": [...] }
  },
  "score": 89,
  "grade": "B",
  "grade_label": "Low-moderate risk",
  "sa_score": null,
  "sa_complete": false,
  "combined_score": null,
  "combined_grade": "B",
  "self_assessment": {},
  "certiq_ready": true,
  "ready_threshold": 65,
  "completed_at": "2026-04-20T12:34:56Z",
  "report_url": "https://certiq.au/api/v1/scans/a1b2c3d4e5f6…/report"
}

Field reference

Field Description
score External score 0–100. Derived from the five signals.
grade Letter grade A–F mapped from the effective score (combined if present, else external).
grade_label Human-readable risk band (e.g. "Low-moderate risk").
sa_score Self-assessment score 0–100, or null until the insured completes the SA.
sa_complete Boolean. True when the insured has finished the self-assessment.
combined_score Weighted hybrid of external + SA, or null while sa_complete is false.
certiq_ready True if the effective score clears the ready threshold (quote-eligible).
ready_threshold Current threshold constant (65).
signals Object keyed by signal name. Each entry has status, score, max_score, findings[].
report_url Authenticated endpoint that streams the branded PDF (see next section).

Signal reference

The signals object contains one entry per check. Each entry carries a score out of max_score and a findings[] array describing specific issues and fixes.

Signal What it measures Max
scan Open ports & services. Internet-exposed services and open ports an attacker could probe. 25
breach Credential breach exposure. Known breach databases checked for leaked passwords tied to this domain. 20
reputation Domain reputation. Cross-referenced against security vendors for malicious flags. 20
mail Email authentication. SPF, DKIM and DMARC records verified to gauge email spoofing risk. 20
layer TLS certificate health. Certificate validity, strength, and configuration inspected. 15

Max scores sum to 100 — the score field at the top of the response is the sum of per-signal scores. Signal names are stable; new signals will be added with a new key rather than changing an existing one.

Download the report PDF

GET /api/v1/scans/:token/report

Streams the scan's PDF report as application/pdf. The PDF is co-branded with your firm's logo and contact card (configure these in partner branding) and includes CertIQ's scoring methodology as the report author.

curl https://certiq.au/api/v1/scans/a1b2c3d4e5f6…/report \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -o report.pdf

Returns 409 Conflict with {"error":"Scan is still running"} if the scan has not yet reached complete.

Webhooks

As an alternative (or complement) to polling, CertIQ POSTs a signed JSON payload to your configured webhook URL when a scan reaches a notable state. Configure your URL and view recent deliveries in partner webhook settings.

Events

Event Fires when
scan.completed External scan reaches complete status and a score is available.
scan.self_assessment_completed Insured completes the self-assessment; combined_score is now populated.
webhook.test Synthetic event fired from the dashboard's Send test event button.

Headers

  • Content-Type: application/json
  • User-Agent: CertIQ-Webhook/1.0
  • X-CertIQ-Event: scan.completed — the event name
  • X-CertIQ-Signature: sha256=<hex> — HMAC-SHA256 of the raw body, keyed with your webhook secret

Payload — scan.completed

{
  "event": "scan.completed",
  "scan_token": "a1b2c3d4e5f6…",
  "status": "complete",
  "score": 89,
  "combined_score": null,
  "sa_complete": false,
  "grade": "B",
  "report_url": "https://certiq.au/scans/a1b2c3d4e5f6…/report.pdf",
  "completed_at": "2026-04-20T12:34:56Z"
}

Delivery & retries

  • 10-second HTTP timeout.
  • Retry on any non-2xx response or network error. Up to 8 attempts with exponential backoff (~1 hour cap).
  • Every delivery attempt is logged and visible in the dashboard's Recent deliveries table.
  • Make your handler idempotent — deduplicate by scan_token + event.

Verifying the signature

Always verify the signature before trusting a payload. Compute HMAC-SHA256 of the raw request body using your webhook secret, then compare against the X-CertIQ-Signature header using a constant-time comparison.

Ruby (Rack)
require "openssl"

secret    = ENV.fetch("CERTIQ_WEBHOOK_SECRET")
raw_body  = request.body.read
signature = request.headers["X-CertIQ-Signature"].to_s # "sha256=abcd…"
expected  = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)

unless ActiveSupport::SecurityUtils.secure_compare(signature, expected)
  head :unauthorized and return
end

payload = JSON.parse(raw_body)
# ... handle payload
Node (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();

app.post(
  "/webhooks/certiq",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const secret = process.env.CERTIQ_WEBHOOK_SECRET;
    const sig = req.get("X-CertIQ-Signature") || "";
    const expected =
      "sha256=" + crypto.createHmac("sha256", secret).update(req.body).digest("hex");

    const a = Buffer.from(sig);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).end();
    }

    const payload = JSON.parse(req.body.toString("utf8"));
    // ... handle payload
    res.status(200).end();
  }
);
Python (Flask)
import hmac, hashlib, os, json
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/webhooks/certiq")
def certiq_webhook():
    secret = os.environ["CERTIQ_WEBHOOK_SECRET"].encode()
    raw = request.get_data()  # raw bytes, before JSON parsing
    sig = request.headers.get("X-CertIQ-Signature", "")
    expected = "sha256=" + hmac.new(secret, raw, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(sig, expected):
        abort(401)

    payload = json.loads(raw)
    # ... handle payload
    return "", 200

Rate limits

The default limit is 100 requests per API key per rolling 24 hours, applied across all /api/* endpoints. When exceeded, CertIQ responds with:

HTTP/1.1 429 Too Many Requests
Retry-After: 86400
Content-Type: application/json

{"error":"Rate limit exceeded","retry_after":86400}

Need a higher limit? Email hello@certiq.au with your expected volume and use case — limits are lifted on request for production partners.

Errors

Error responses share a consistent JSON shape:

{ "error": "Invalid domain" }
Status Meaning
401 Unauthorised Missing, malformed, inactive, or unknown API key.
404 Not Found Scan token does not exist.
409 Conflict Requested the PDF of a scan that hasn't completed.
422 Unprocessable Entity Request body failed validation (e.g. invalid domain).
429 Too Many Requests Rate limit exceeded. Check Retry-After header.
5xx CertIQ-side issue. Retry with exponential backoff.

SDK snippets

There is no official SDK yet — the API surface is small enough that any HTTP client will do. The snippets below show a typical create-and-poll loop.

Ruby (Faraday)
require "faraday"
require "json"

client = Faraday.new("https://certiq.au/api/v1") do |f|
  f.request :json
  f.response :json
  f.headers["Authorization"] = "Bearer #{ENV.fetch("CERTIQ_API_KEY")}"
end

created = client.post("scans", { domain: "example.com.au" }).body
token   = created["scan_token"]

loop do
  scan = client.get("scans/#{token}").body
  break scan if scan["status"] == "complete"
  raise "scan failed" if scan["status"] == "failed"
  sleep 5
end
Node (fetch)
const BASE = "https://certiq.au/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.CERTIQ_API_KEY}`,
  "Content-Type": "application/json"
};

const created = await fetch(`${BASE}/scans`, {
  method: "POST",
  headers,
  body: JSON.stringify({ domain: "example.com.au" })
}).then((r) => r.json());

const token = created.scan_token;
let scan;
do {
  await new Promise((r) => setTimeout(r, 5000));
  scan = await fetch(`${BASE}/scans/${token}`, { headers }).then((r) => r.json());
} while (scan.status !== "complete" && scan.status !== "failed");
Python (requests)
import os, time, requests

BASE = "https://certiq.au/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['CERTIQ_API_KEY']}"

created = session.post(f"{BASE}/scans", json={"domain": "example.com.au"}).json()
token = created["scan_token"]

while True:
    scan = session.get(f"{BASE}/scans/{token}").json()
    if scan["status"] == "complete":
        break
    if scan["status"] == "failed":
        raise RuntimeError("scan failed")
    time.sleep(5)

Changelog & versioning

The API is versioned in the URL. v1 is the current contract. Breaking changes will ship under /api/v2 with a deprecation window and migration notes. Additive changes (new fields, new events) land in v1 without version bumps — build tolerant parsers.

v1 · beta
Scans · webhooks · branded PDF · partner dashboard.

Need sandbox credentials or a higher limit?

Email us with your use case — we respond within a business day.

hello@certiq.au