Semhack API

Pull audits, site audits, websites, clients, Search Console summaries and the weekly action queue into your own dashboards, scripts and automations, start audits, close actions, and get webhook events when things happen. JSON over HTTPS, versioned under /api/v1.

Base URL

https://semhack.com/api/v1

OpenAPI 3.1

https://semhack.com/api/v1/openapi.json

Keys

Settings → API & webhooks

The OpenAPI document is generated from the same definitions the API validates requests with, so it is always current. Import it into Postman, Insomnia or a client generator.

Authentication

Create a key in Settings → API & webhooks (owners and admins) and send it as a bearer token. Keys start with smh_live_ and are shown once: Semhack stores only a SHA-256 hash, so a lost key can't be recovered, only revoked and replaced.
Shell
curl https://semhack.com/api/v1/me \
  -H "Authorization: Bearer $SEMHACK_API_KEY"
ScopeAllows
readEvery GET endpoint.
writeAlso starting audits and site audits, and changing an action's status. Write keys can read too.

A key sees the whole workspace it was created in and nothing else. Keep it on a server, never in a browser or app: the API doesn't send CORS headers, so browsers can't call it from another site. Revoking a key works immediately.

Errors & pagination

Errors use HTTP status codes and one JSON shape. code is stable and safe to branch on; message is written for people. Quote request_id (also in the X-Request-Id header) when you contact us.

400 Bad Request
{
  "error": {
    "code": "invalid_request",
    "message": "Invalid query.",
    "details": [{ "path": "query.limit", "message": "Too big: expected number to be <=100" }],
    "request_id": "req_Qk1x9mZ2aB7c"
  }
}
StatusWhen
400 invalid_requestMalformed query, path or body; details lists each problem.
401 unauthorizedMissing, malformed, unknown or revoked key.
402 plan_limitYour plan's audits per month (or websites) are used up; upgrade_url links to Billing.
403 forbiddenThe key lacks the scope the endpoint needs.
404 not_foundNo such resource in your workspace (other workspaces' ids look exactly like missing ones).
413 payload_too_largeBodies over 16 KB.
422 unprocessableValid JSON that can't be done, e.g. a URL that can't be audited.
429 rate_limitedToo many requests; wait for Retry-After seconds.

Lists take limit (1–100, default 20) and offset and return { data: [...], pagination: { limit, offset, total, has_more, next_offset } }. Request the next page with offset=next_offset until it is null. Timestamps are ISO 8601 in UTC; ids are UUIDs.

Rate limits

LimitApplies to
120 requests per minuteEach API key, all endpoints together.
20 failed requests per minuteEach IP address sending an invalid or revoked key; further requests get 429 for a minute.
20 audits per 10 minutesEach workspace (app and API together), plus your plan's audits per month.
10 site audits per hourEach workspace. Pages per site audit follow your plan.

Every authenticated response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds). A 429 adds Retry-After.

Account

Check which workspace a key belongs to.
GET/api/v1/mescope: read

The workspace and key you are calling with

Handy for checking that a key works.

Shell
curl "https://semhack.com/api/v1/me" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a Me (schema in the OpenAPI spec).

Audits

Single-page audits: 40+ technical and on-page checks with a 0–100 score.
GET/api/v1/auditsscope: read

List audits

Newest first.

limitquery · integer 1–100
Items per page, 1–100. Default 20.
offsetquery · integer 0–100000
How many items to skip. Default 0.
statusquery · "queued" | "running" | "done" | "failed"
website_idquery · string
Only audits of this website.
qquery · string
Substring of the URL, case-insensitive.
Shell
curl "https://semhack.com/api/v1/audits?status=done&limit=10" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a AuditList (schema in the OpenAPI spec).

POST/api/v1/auditsscope: write

Audit a page

Queues an audit of one URL and returns it with status queued. Poll GET /audits/{id} or subscribe to the audit.completed webhook. Counts toward your plan's audits per month (402 plan_limit when used up); a workspace can also start at most 20 audits per 10 minutes.

urlrequiredbody · string
The page to audit, e.g. example.com/pricing.
Shell
curl -X POST "https://semhack.com/api/v1/audits" \
  -H "Authorization: Bearer $SEMHACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "example.com/pricing"}'

Returns 202 with a Audit (schema in the OpenAPI spec).

GET/api/v1/audits/{id}scope: read

Get an audit with its checks

idrequiredpath · string
Resource id.
Shell
curl "https://semhack.com/api/v1/audits/3f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a AuditDetail (schema in the OpenAPI spec).

Site audits

Multi-page crawls of a whole website with site-wide issues like broken links and duplicate titles.
GET/api/v1/site-auditsscope: read

List site audits

Multi-page crawls, newest first.

limitquery · integer 1–100
Items per page, 1–100. Default 20.
offsetquery · integer 0–100000
How many items to skip. Default 0.
website_idquery · string
Only site audits of this website.
Shell
curl "https://semhack.com/api/v1/site-audits?limit=5" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a SiteAuditList (schema in the OpenAPI spec).

POST/api/v1/site-auditsscope: write

Start a site audit

Crawls the website the URL belongs to, starting at its homepage, up to your plan's pages per site audit. If one is already queued or running for that website, returns it instead of starting another. Subscribe to site_audit.completed, or poll GET /site-audits/{id}. At most 10 new site audits per hour per workspace.

urlrequiredbody · string
Any URL of the website, e.g. example.com.
page_limitbody · integer 1–5000
Crawl fewer pages than your plan allows.
Shell
curl -X POST "https://semhack.com/api/v1/site-audits" \
  -H "Authorization: Bearer $SEMHACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "example.com"}'

Returns 202 with a SiteAudit (schema in the OpenAPI spec).

GET/api/v1/site-audits/{id}scope: read

Get a site audit with its issues

idrequiredpath · string
Resource id.
Shell
curl "https://semhack.com/api/v1/site-audits/3f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a SiteAuditDetail (schema in the OpenAPI spec).

Websites

Every site you have audited.
GET/api/v1/websitesscope: read

List audited websites

Most recently audited first.

limitquery · integer 1–100
Items per page, 1–100. Default 20.
offsetquery · integer 0–100000
How many items to skip. Default 0.
Shell
curl "https://semhack.com/api/v1/websites" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a WebsiteList (schema in the OpenAPI spec).

Clients

Client websites and their Search Console data.
GET/api/v1/clientsscope: read

List clients

Client websites and their Search Console status, oldest first.

limitquery · integer 1–100
Items per page, 1–100. Default 20.
offsetquery · integer 0–100000
How many items to skip. Default 0.
Shell
curl "https://semhack.com/api/v1/clients" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a ClientList (schema in the OpenAPI spec).

GET/api/v1/clients/{id}/performancescope: read

Search Console performance summary

Clicks, impressions, CTR and position for the last 28 days with data and the 28 days before. 404 until data has been imported.

idrequiredpath · string
Resource id.
Shell
curl "https://semhack.com/api/v1/clients/3f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f/performance" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a Performance (schema in the OpenAPI spec).

Findings

The weekly action queue: what to fix first, with clicks at stake from Search Console.
GET/api/v1/findingsscope: read

This week's actions

view=week (default) is the weekly queue the app shows: each client's top five actions, ranked by score. view=all lists every finding with the given statuses.

limitquery · integer 1–100
Items per page, 1–100. Default 20.
offsetquery · integer 0–100000
How many items to skip. Default 0.
viewquery · "week" | "all"
Default week.
client_idquery · string
Only this client's findings.
statusquery · string
Comma-separated. Default open,doing. Only for view=all.
typequery · string
Comma-separated finding types.
Shell
curl "https://semhack.com/api/v1/findings?view=all&status=open,doing&type=low-ctr" \
  -H "Authorization: Bearer $SEMHACK_API_KEY"

Returns 200 with a FindingList (schema in the OpenAPI spec).

PATCH/api/v1/findings/{id}scope: write

Change a finding's status

Mark an action done, dismiss it, or reopen it. Sends finding.status_changed to your webhooks when the status changes.

idrequiredpath · string
Resource id.
statusrequiredbody · "open" | "done" | "dismissed"
done or dismissed takes it out of the queue; open puts it back (and clears its assignee).
Shell
curl -X PATCH "https://semhack.com/api/v1/findings/3f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f" \
  -H "Authorization: Bearer $SEMHACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "done"}'

Returns 200 with a Finding (schema in the OpenAPI spec).

Webhooks

Add an https endpoint in Settings → API & webhooks, pick its events, and Semhack POSTs a JSON event to it. Up to 10 endpoints per workspace. URLs that point at private or local networks are refused, and redirects are not followed.
EventSent when
audit.completedA page audit finished. data.audit has the score and counts.
audit.failedA page audit couldn't run. data.audit.error says why.
site_audit.completedA site audit finished, or was stopped and its crawled pages analyzed (status is cancelled then).
site_audit.failedA site audit couldn't run. data.site_audit.error says why.
finding.createdNew actions for a client: one event per recompute or audit with up to 50 findings, highest score first, and total.
finding.status_changedAn action was marked done, dismissed, reopened or assigned, in the app or through the API. Includes previous_status.
monitoring.alertMonitoring raised alerts: site down or back up, SSL expiring, score drop, a check that started failing, robots or noindex, traffic drop. Only rules turned on in Notifications send events.
webhook.testSent by “Send test” in Settings, to check your endpoint.
POST your-endpoint · application/json
{
  "id": "evt_8c1f6b0f4e2a4d3c9b7a5e1d2c3b4a59",
  "type": "audit.completed",
  "created_at": "2026-11-05T09:41:00.000Z",
  "organization_id": "org_…",
  "data": {
    "audit": {
      "id": "3f0c1d2e-8a4b-4c6d-9e7f-1a2b3c4d5e6f",
      "url": "https://example.com/pricing",
      "host": "example.com",
      "status": "done",
      "score": 82,
      "counts": { "pass": 31, "warn": 6, "fail": 3, "info": 4, "total": 44 },
      "…": "see the Audit schema in the OpenAPI spec"
    }
  }
}
HeaderValue
Semhack-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>, see below
Semhack-EventThe event type.
Semhack-DeliveryThis delivery's id; differs on every resend.

Answer with any 2xx within 10 seconds. Anything else, a timeout or a redirect counts as a failure and is retried 5 times, after 1 min, 5 min, 30 min, 2 h, 8 h. The delivery log in Settings shows every attempt with its status code, and you can resend any delivery. An endpoint whose deliveries fail 20 times in a row is paused until you resume it. Events can arrive more than once and out of order: de-duplicate on id and use created_at to order them.

Verifying signatures

Each endpoint has a signing secret (whsec_…). Semhack computes an HMAC-SHA256 of <t>.<raw body> with it and sends Semhack-Signature: t=<t>,v1=<hex>. Recompute it over the raw request body, compare in constant time, and reject timestamps more than 5 minutes away so old deliveries can't be replayed. After you rotate a secret the old one stops working at once.
Node.js · Express
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.SEMHACK_WEBHOOK_SECRET; // whsec_…, from Settings → API
const TOLERANCE_SECONDS = 300;

function verifySemhackSignature(secret, header, rawBody, now = Date.now()) {
  if (!header) return false;
  const parts = header.split(",").map((p) => p.trim().split("="));
  const t = Number(parts.find(([k]) => k === "t")?.[1]);
  const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v ?? "");
  if (!Number.isInteger(t) || signatures.length === 0) return false;
  // Reject old (replayed) deliveries.
  if (Math.abs(Math.floor(now / 1000) - t) > TOLERANCE_SECONDS) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest();
  return signatures.some((s) => {
    const given = Buffer.from(s, "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

const app = express();

// Verify against the raw body: re-serialized JSON wouldn't match the signature.
app.post("/semhack-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  if (!verifySemhackSignature(SECRET, req.get("Semhack-Signature"), rawBody)) {
    return res.status(400).send("Invalid signature");
  }
  const event = JSON.parse(rawBody);
  // Deliveries can repeat (retries, "Resend"): de-duplicate on event.id.
  switch (event.type) {
    case "audit.completed":
      console.log(`${event.data.audit.url} scored ${event.data.audit.score}`);
      break;
    case "finding.created":
      console.log(`${event.data.total} new actions for ${event.data.client.name}`);
      break;
  }
  res.sendStatus(204); // Answer within 10 seconds; do slow work afterwards.
});

app.listen(3000);

Slack

Choose Slack channel when adding an endpoint and paste an incoming-webhook URL from Slack (https://hooks.slack.com/services/…). The same events arrive as readable messages with links into Semhack. Slack messages aren't signed; the webhook URL itself is the secret, so Semhack never shows it in full after you save it.

Questions or something missing? Email hello@semhack.com.