Skip to content

API documentation

With the CheckAI API you read your businesses, checks and every single AI answer from your own tools, manage your tracked questions, start checks and ask on-demand questions. Webhooks tell you when a check has finished or when your named rate moves by more than normal variation.

The API is part of Pro. It answers in JSON; times are UTC (ISO 8601).

Quick start

  1. Create a key under Account → API and webhooks. It is shown only once.
  2. List your businesses:
curl https://checkai.ch/api/v1/businesses \
  -H "Authorization: Bearer sl_live_..."
  1. Read the latest check (latest_check.id) with GET /checks/{id} and its answers with GET /checks/{id}/answers.

Base URL: https://checkai.ch/api/v1

Authentication

Send your key in the Authorization header on every request:

Authorization: Bearer sl_live_3q9...
  • Keys start with sl_live_. We store only a fingerprint, so a lost key can't be shown again: create a new one and revoke the old one.
  • Up to 5 active keys per account. A key stops working when it is revoked or when the account is no longer on Pro (403 plan_required).
  • Use keys only on your server. The API doesn't accept browser sessions and doesn't send CORS headers, so it can't be called from a web page.

Limits

WhatLimit
Requests120 per minute per key. Above that: 429 rate_limited with a Retry-After header (seconds).
Full checks you start7 extra checks per 7 days across your account, on top of the scheduled daily checks.
On-demand questions500 per calendar month per account. Each is asked to all four assistants.
Tracked questions50 per business on Pro, plus 25 per bought add-on.
Search terms5 per business.
Listslimit up to 100 per page.

Errors

Every error has the same shape, with an HTTP status and a stable code:

{
  "error": {
    "code": "limit_reached",
    "message": "This business already tracks 50 questions, the most your plan allows."
  }
}
StatuscodeMeaning
400bad_request, limit_reachedSomething in the request is missing or over a limit. The message says what.
401unauthorizedNo key, or the key is wrong or revoked.
402 / 403plan_requiredThe account isn't on Pro.
404not_foundNo such business, check or question in your account.
409existsThe question is already tracked.
422(validation)A query parameter has the wrong type, e.g. limit=abc.
429rate_limited, limit_reachedToo many requests, or the check / on-demand limit is used up.
503capacityWe can't start a check right now. Try again later.

Objects

Check

One run of questions to ChatGPT, Google AI Mode, Gemini and Copilot. Lists and webhooks send this summary; GET /checks/{id} adds the details below.

FieldTypeDescription
idstringCheck id. Also the id in the report link.
business_idstringThe business it belongs to.
kindstringscheduled (daily on Pro), manual (started by you), on_demand (one question).
statusstringqueued, running, done or failed. A check takes about 60 to 90 seconds.
created_at, finished_atstringUTC, ISO 8601. finished_at is null until done.
questions, answersintegerQuestions asked and answers received (questions × assistants, minus any that didn't come back).
namedintegerAnswers that name your business. Named means mentioned, not necessarily recommended.
named_ratenumbernamed / answers, 0 to 1.
likely_range[number, number]90% Wilson interval for the named rate. AI answers vary, so read the rate with this range.
average_positionnumber or nullAverage place in the list of businesses, over the answers that name you. 1 = named first.
share_of_mentionsnumberYour mentions out of all business mentions (names you marked as "not a competitor" are left out).
recommendedintegerAnswers that name you and recommend or praise you.
own_site_citedintegerAnswers that link to your website.
assistantsobjectPer assistant (chatgpt, google_ai_mode, gemini, bing_copilot): answers, named, average_position.
report_urlstringThe report in the browser.
{
  "id": "xFIQFlSX5v5H",
  "business_id": "8f2c1a7d90b3e451",
  "kind": "scheduled",
  "status": "done",
  "created_at": "2026-10-14T04:08:25Z",
  "finished_at": "2026-10-14T04:09:38Z",
  "questions": 6,
  "answers": 24,
  "named": 23,
  "named_rate": 0.9583,
  "likely_range": [0.8331, 0.9906],
  "average_position": 2.0,
  "share_of_mentions": 0.1264,
  "recommended": 19,
  "own_site_cited": 11,
  "assistants": {
    "chatgpt":        {"label": "ChatGPT",        "answers": 6, "named": 6, "average_position": 1.7},
    "google_ai_mode": {"label": "Google AI Mode", "answers": 6, "named": 6, "average_position": 1.8},
    "gemini":         {"label": "Gemini",         "answers": 6, "named": 6, "average_position": 2.3},
    "bing_copilot":   {"label": "Copilot",        "answers": 6, "named": 5, "average_position": 2.0}
  },
  "report_url": "https://checkai.ch/r/xFIQFlSX5v5H"
}

Check details

GET /checks/{id} returns the check plus:

FieldDescription
namesEvery business named in the answers: name, answers (in how many answers), assistants (how many of the four named it), average_position, is_you, not_competitor.
sourcesWebsites the answers linked to: domain, kind (own, directory, editorial, community, competitor), answers, example_url, mentions_you (true / false after we read the page, null if we couldn't).
searchGoogle and Bing position per search term: term, engine, position (null = not in the top 10), ai_overview (whether Google showed one).
what_assistants_sayAnswers to "what is your business?": per assistant knows (yes, no, other), summary, tone; and statements with verdict (supported, contradicted, not_on_site) compared with your website. Null for on-demand questions.
suggestionsUp to 3 things to check: title, observed, check, condition, domain, question.
structured_dataReady-to-paste company details for your homepage: status (ok, incomplete, missing), code, missing, added.
"names": [
  {"name": "bexio", "answers": 23, "assistants": 4, "average_position": 2.0, "is_you": true,  "not_competitor": false},
  {"name": "KLARA", "answers": 19, "assistants": 4, "average_position": 3.3, "is_you": false, "not_competitor": false}
],
"sources": [
  {"domain": "klara.ch", "kind": "competitor", "answers": 8, "example_url": "https://www.klara.ch/", "mentions_you": true}
],
"search": [
  {"term": "erp software kmu", "engine": "google", "status": "done", "position": 1, "ai_overview": false}
],
"what_assistants_say": {
  "question": "Was ist bexio? Was bietet das Unternehmen an, für wen und wo, und was kostet es ungefähr?",
  "assistants": {"chatgpt": {"knows": "yes", "summary": "bexio wird als Schweizer Softwareunternehmen beschrieben, …", "tone": "neutral"}},
  "statements": [
    {"assistant": "chatgpt", "statement": "Über 100'000 Kunden nutzen bexio.", "quote": "…", "verdict": "supported", "website_says": null}
  ]
}

Answer

FieldDescription
id, question, assistantWhich question went to which assistant.
statusdone or failed (the assistant gave no usable answer).
named, positionWhether your business is named, and where in the list.
namesAll businesses the answer names, in order.
sourcesLinks in the answer: domain, url, title.
textThe full answer as Markdown.
captured_atWhen the answer was received.
{
  "id": "ca2a3dda58b1715f",
  "question": "Welche Business Software für Schweizer KMU wird aktuell am besten bewertet",
  "assistant": "google_ai_mode",
  "status": "done",
  "named": true,
  "position": 1,
  "names": ["bexio", "KLARA", "Proffix", "Abacus", "Banana Accounting", "AtlantO"],
  "sources": [{"domain": "klara.ch", "url": "https://www.klara.ch/", "title": "Business Software für KMU in der Schweiz | KLARA"}],
  "text": "Die am besten bewerteten und beliebtesten Business-Software-Lösungen …",
  "captured_at": "2026-10-14T04:08:34Z"
}

Account

GET /me

Your plan and what's used.

{
  "email": "you@example.com",
  "plan": "pro",
  "businesses": {"used": 2, "limit": 3},
  "on_demand_questions": {"used_this_month": 4, "limit": 500},
  "rate_limit_per_minute": 120
}

Businesses

GET /businesses

All businesses in your account, each with its latest scheduled or manual check (on-demand questions don't count as the latest check).

{
  "data": [
    {
      "id": "8f2c1a7d90b3e451",
      "name": "Velo Huber",
      "website": "velohuber.ch",
      "offering": "Velogeschäft",
      "market": "Bern",
      "country": "CH",
      "language": "de",
      "tracking": {"active": true, "every_days": 7, "daily_addon": false, "next_check_at": "2026-10-21T06:00:00Z"},
      "questions_tracked": 30,
      "search_terms": ["velo bern", "e-bike reparatur bern"],
      "latest_check": { …Check… }
    }
  ]
}

GET /businesses/{business_id}

One business, plus questions: every question with id, text and tracked (false = switched off, kept for history).

Tracked questions

The questions asked at every scheduled check. Changes apply from the next check on.

GET /businesses/{business_id}/questions

{
  "data": [{"id": "3b9e0c51a2f4d871", "text": "Which bike shop in Bern repairs e-bikes the same day?", "tracked": true}],
  "limit": 50
}

POST /businesses/{business_id}/questions

Add one question (8 to 300 characters). A question you switched off earlier is switched on again, with its history. Returns 201.

curl -X POST https://checkai.ch/api/v1/businesses/8f2c1a7d90b3e451/questions \
  -H "Authorization: Bearer sl_live_..." -H "Content-Type: application/json" \
  -d '{"text": "Which bike shop in Bern repairs e-bikes the same day?"}'

PUT /businesses/{business_id}/questions

Replace the whole tracked list. Questions not in the list stop being tracked.

{"questions": ["Which bike shop in Bern repairs e-bikes the same day?", "Where can I buy a cargo bike in Bern?"]}

DELETE /businesses/{business_id}/questions/{question_id}

Stop tracking a question. Returns 204; its past answers stay.

PUT /businesses/{business_id}/search-terms

Up to 5 Google and Bing search terms, checked with every full check.

{"terms": ["velo bern", "e-bike reparatur bern"]}

Checks

GET /businesses/{business_id}/checks

Checks of a business, newest first. Query parameters:

  • limit: 1 to 100, default 20.
  • before: a created_at value; returns older checks (use next_before from the previous page).
  • kind: all (default), tracked (scheduled and manual), on_demand.
{"data": [ …Check… ], "has_more": true, "next_before": "2026-09-30T06:00:12Z"}

POST /businesses/{business_id}/checks

Start a full check now with all tracked questions and search terms. Returns 202 with the check (status queued). Poll GET /checks/{id} every 10 seconds, or wait for the check.completed webhook. One extra check per business per 7 days.

GET /checks/{check_id}

The check with all details (see Check details).

GET /checks/{check_id}/answers

Every answer of the check, with the full text: {"data": [ …Answer… ]}.

On-demand questions

POST /businesses/{business_id}/ask

Ask one question of your own to all four assistants right away, without adding it to the tracked questions. It becomes a check with kind on_demand; it isn't part of the trend, the weekly email or visibility.changed. 500 per month; the response says how many are left.

curl -X POST https://checkai.ch/api/v1/businesses/8f2c1a7d90b3e451/ask \
  -H "Authorization: Bearer sl_live_..." -H "Content-Type: application/json" \
  -d '{"question": "Which bike shop in Bern repairs e-bikes the same day?"}'
HTTP/1.1 202 Accepted
{ …Check, "kind": "on_demand", "status": "queued"…, "on_demand_left": 25 }

Webhooks

Add an https address under Account → API and webhooks and choose the events. We send a POST with a JSON body:

EventWhendata
check.completedAny check of yours has finished (scheduled, manual or on-demand).business (id, name), check
visibility.changedA scheduled or manual check's named rate differs from the previous one by more than normal variation (two-proportion test at 90%).business, check, change: previous (check), points, direction (up / down)
pingOnly when you press "Send test".message
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: CheckAI-Webhooks/1
CheckAI-Event: visibility.changed
CheckAI-Delivery: evt_5f0c2b8e19a4d7c3e6b1a902
CheckAI-Signature: t=1792130400,v1=5d41402abc4b2a76b9719d911017c592...

{
  "id": "evt_5f0c2b8e19a4d7c3e6b1a902",
  "type": "visibility.changed",
  "created_at": "2026-10-14T06:01:40Z",
  "api_version": "2026-10-14",
  "data": {
    "business": {"id": "8f2c1a7d90b3e451", "name": "Velo Huber"},
    "check": { …Check… },
    "change": {"previous": { …Check… }, "points": 25.0, "direction": "up"}
  }
}
  • Answer with any 2xx status within 10 seconds; do slow work afterwards.
  • If we don't get a 2xx, we try again after 30 seconds and after 5 minutes. A webhook that fails 15 events in a row is switched off; send a test from the account page to switch it on again.
  • The same event can arrive twice. Use id to ignore repeats. Events can arrive out of order; use created_at.
  • We only deliver to public https addresses on port 443, and we don't follow redirects.

Verifying signatures

Every request is signed with your webhook's secret (whsec_…, shown once when you add the webhook). The header is t=<unix time>,v1=<signature>, where the signature is HMAC-SHA256 of <t>.<raw body> in hex. Check it against the raw body before parsing, and reject old timestamps.

Python

import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Node.js

const crypto = require("crypto");

function verify(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return typeof parts.v1 === "string" && parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Versions

This is version 2026-10-14 (also in every webhook as api_version). We add fields and endpoints without notice, so ignore fields you don't know. Anything that would break an existing integration gets a new version and an email to Pro customers first.