API-Dokumentation
Mit der CheckAI-API lesen Sie Ihre Unternehmen, Checks und jede einzelne KI-Antwort aus Ihren eigenen Tools, verwalten Ihre verfolgten Fragen, starten Checks und stellen spontane Fragen. Webhooks melden sich, sobald ein Check fertig ist oder sich Ihre Nennungsrate mehr als normal verändert.
Die API ist Teil von Pro. Sie antwortet mit JSON; Zeitangaben sind in UTC (ISO 8601). Die technische Referenz unten ist auf Englisch, damit Feldnamen, Fehlercodes und Beispiele eindeutig bleiben.
Schnellstart
- Erstellen Sie unter Konto → API und Webhooks einen Schlüssel. Er wird nur einmal angezeigt.
- Rufen Sie Ihre Unternehmen ab:
curl https://checkai.ch/api/v1/businesses \
-H "Authorization: Bearer sl_live_..."
- Lesen Sie den neusten Check (
latest_check.id) mitGET /checks/{id}und seine Antworten mitGET /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
| What | Limit |
|---|---|
| Requests | 120 per minute per key. Above that: 429 rate_limited with a Retry-After header (seconds). |
| Full checks you start | 7 extra checks per 7 days across your account, on top of the scheduled daily checks. |
| On-demand questions | 500 per calendar month per account. Each is asked to all four assistants. |
| Tracked questions | 50 per business on Pro, plus 25 per bought add-on. |
| Search terms | 5 per business. |
| Lists | limit 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."
}
}
| Status | code | Meaning |
|---|---|---|
| 400 | bad_request, limit_reached | Something in the request is missing or over a limit. The message says what. |
| 401 | unauthorized | No key, or the key is wrong or revoked. |
| 402 / 403 | plan_required | The account isn't on Pro. |
| 404 | not_found | No such business, check or question in your account. |
| 409 | exists | The question is already tracked. |
| 422 | (validation) | A query parameter has the wrong type, e.g. limit=abc. |
| 429 | rate_limited, limit_reached | Too many requests, or the check / on-demand limit is used up. |
| 503 | capacity | We 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.
| Field | Type | Description |
|---|---|---|
id | string | Check id. Also the id in the report link. |
business_id | string | The business it belongs to. |
kind | string | scheduled (daily on Pro), manual (started by you), on_demand (one question). |
status | string | queued, running, done or failed. A check takes about 60 to 90 seconds. |
created_at, finished_at | string | UTC, ISO 8601. finished_at is null until done. |
questions, answers | integer | Questions asked and answers received (questions × assistants, minus any that didn't come back). |
named | integer | Answers that name your business. Named means mentioned, not necessarily recommended. |
named_rate | number | named / 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_position | number or null | Average place in the list of businesses, over the answers that name you. 1 = named first. |
share_of_mentions | number | Your mentions out of all business mentions (names you marked as "not a competitor" are left out). |
recommended | integer | Answers that name you and recommend or praise you. |
own_site_cited | integer | Answers that link to your website. |
assistants | object | Per assistant (chatgpt, google_ai_mode, gemini, bing_copilot): answers, named, average_position. |
report_url | string | The 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:
| Field | Description |
|---|---|
names | Every 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. |
sources | Websites 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). |
search | Google and Bing position per search term: term, engine, position (null = not in the top 10), ai_overview (whether Google showed one). |
what_assistants_say | Answers 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. |
suggestions | Up to 3 things to check: title, observed, check, condition, domain, question. |
structured_data | Ready-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
| Field | Description |
|---|---|
id, question, assistant | Which question went to which assistant. |
status | done or failed (the assistant gave no usable answer). |
named, position | Whether your business is named, and where in the list. |
names | All businesses the answer names, in order. |
sources | Links in the answer: domain, url, title. |
text | The full answer as Markdown. |
captured_at | When 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: acreated_atvalue; returns older checks (usenext_beforefrom 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:
| Event | When | data |
|---|---|---|
check.completed | Any check of yours has finished (scheduled, manual or on-demand). | business (id, name), check |
visibility.changed | A 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) |
ping | Only 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
2xxstatus 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
idto ignore repeats. Events can arrive out of order; usecreated_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.