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
- Create a key under Account → API and webhooks. It is shown only once.
- List your businesses:
curl https://checkai.ch/api/v1/businesses \
-H "Authorization: Bearer sl_live_..."
- Read the latest check (
latest_check.id) withGET /checks/{id}and its answers withGET /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.