API for AI agents¶
How an AI agent, a script or a workflow tool uses the AskAniela API step by step: key, one page, a site audit, search intent, costs, errors and the JSON of the results.
Every example on this page was run against our staging API before publication. The Markdown version of this page
is at https://docs.askaniela.com/user/agents.md.
Who this is for¶
- AI agents with an HTTP tool (Claude, ChatGPT, Codex, Cursor and similar): give the agent this page, the example system prompt and a key in an environment variable.
- Scripts (Python, Node, shell): copy the curl or Python examples.
- Workflow tools (n8n, Make, Zapier): use the HTTP Request node with
Authorization: Bearer gsj_..., a Wait node and a loop on the status field. - MCP: an AskAniela MCP server is planned on top of the same API, with the same keys and limits. Until then,
connect the API as an HTTP tool or generate tools from
openapi.json(see What agents can read).
Before the first call¶
| What | Value |
|---|---|
| Base URL | https://api.askaniela.com |
| Header | Authorization: Bearer gsj_... on every request (no key: 401 unauthorized) |
| Body | JSON, Content-Type: application/json |
| Key | a project key from the project's API keys tab (API keys) |
| Scopes | judge, reports:read, jobs (the defaults) cover everything on this page |
Keys created before the defaults changed (October 2026) may lack jobs. Then site audits answer 403 insufficient_scope with
error.details.required_scope; create a new key with the default scopes. Give each agent its own key with an
expiry date, keep it in an environment variable and never put it into a prompt, a log or a chat.
export ASKANIELA_URL=https://api.askaniela.com
export ASKANIELA_API_KEY=gsj_...
Map of tasks¶
| Task | Call | Scope | Answer | Paid |
|---|---|---|---|---|
| Check one page | POST /v1/judge |
judge |
200, report in the body (1-3 min) |
yes |
| Check up to 10 pages | POST /v1/judge/batch |
judge |
200, results[] |
yes |
| Price of a site audit | POST /v1/audits/site/preview, then /estimate |
jobs |
200 |
no |
| Site audit | POST /v1/audits/site |
jobs |
202, then poll |
yes |
| Audit status | GET /v1/audits/{id} |
reports:read |
200 |
no |
| Audit results | GET /v1/audits/{id}/export.json |
reports:read |
202 while prepared, then 200 |
no |
| Fix plan | GET /v1/audits/{id}/fix-plan?format=md |
reports:read |
202 while generated, then Markdown |
no |
| Search intent | POST /v1/intent |
judge |
202, then GET /v1/intent/{id} |
yes |
| Balance and limits | GET /v1/usage |
any | 200 |
no |
| Prices | GET /v1/tiers |
none | 200 |
no |
1. Check one page¶
curl -s "$ASKANIELA_URL/v1/judge" \
-H "Authorization: Bearer $ASKANIELA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/"}' \
--max-time 300 -D headers.txt -o report.json
grep -i x-cost-usd headers.txt # x-cost-usd: 0.0665
- Send exactly one of
url,htmlortext. Optional:modes(["spam"],["quality"],["helpful"]or a mix; fewer modes cost less),rules(ids fromGET /v1/rules),render_js,explain(evidence and fixes for hits),report_lang(enorpl),project_id. - The call is synchronous. One page with the full catalogue took about 2 minutes on staging; fewer
modesare faster. Set the client read timeout to at least 300 seconds. - The body has no report id. Find the stored report with
GET /v1/reports?url=https://example.com/&limit=1(id=rep_…), thenGET /v1/reports/{id}/fix-plan?format=md. - More than one page:
POST /v1/judge/batchwith{"items": [{"url": …}, …]}(up to 10), or a site audit.
2. Site audit¶
A site audit checks a sample of pages from a sitemap or your URL list. It runs in the background.
Step 1: preview (free). Finds the sitemap and groups URLs into patterns.
curl -s "$ASKANIELA_URL/v1/audits/site/preview" \
-H "Authorization: Bearer $ASKANIELA_API_KEY" -H "Content-Type: application/json" \
-d '{"site": "example.com"}'
# {"preview_id": "7963…", "total": 94, "patterns": [{"pattern": "/user/<slug>", "count": 11, …}], …}
Step 2: estimate (free, nothing is started). Send the body you want to run, with preview_id.
curl -s "$ASKANIELA_URL/v1/audits/site/estimate" \
-H "Authorization: Bearer $ASKANIELA_API_KEY" -H "Content-Type: application/json" \
-d '{"site": "example.com", "preview_id": "7963…", "pages": 5,
"scope": {"include_patterns": ["/user/<slug>"]}}'
# {"selected": 11, "pages": 5, "rules": 119, "cost_usd": 0.3325, "cost_per_page_usd": 0.0665, "seconds": 50, …}
Step 3: ask the person. Show cost_usd, pages and the time, and wait for a yes. An agent should never
start an audit on its own.
Step 4: start (paid). Send the same body once.
curl -s "$ASKANIELA_URL/v1/audits/site" \
-H "Authorization: Bearer $ASKANIELA_API_KEY" -H "Content-Type: application/json" \
-d '{"site": "example.com", "preview_id": "7963…", "pages": 5,
"scope": {"include_patterns": ["/user/<slug>"]}}'
# 202 {"audit_id": "aud_2fptzm5h3hiruh6c", "job_id": "job_e74cap43wdgzyszz", "pages": 5, "patterns": 1}
Do not repeat this POST after a timeout: first look for the audit in GET /v1/audits?limit=5
(see Retrying safely).
Step 5: wait. Poll GET /v1/audits/{audit_id} every 15-30 seconds until progress.status is final.
The status endpoint sends no Retry-After, so pick the interval yourself; eta_seconds gives the expected time.
curl -s "$ASKANIELA_URL/v1/audits/aud_2fptzm5h3hiruh6c" -H "Authorization: Bearer $ASKANIELA_API_KEY"
# {"progress": {"status": "running", "total": 5, "done": 1, "failed": 0, …}, "eta_seconds": 38, …}
progress.status |
Final | What to do |
|---|---|---|
queued, running |
no | keep polling |
pausing, paused, cancelling |
no | someone paused or stopped it; POST /v1/audits/{id}/resume continues a paused audit |
done |
yes | read the results; failed > 0 means some pages could not be checked |
failed |
yes | the audit stopped (error); pages not judged go back to the balance |
cancelled |
yes | stopped for good; judged pages keep their reports |
The 5-page audit above took about 6 minutes on staging. The same 8 states and transitions are described in Job statuses.
Instead of polling you can receive a webhook. An audit has no webhook_url field; an organization owner or admin
registers an organization webhook once, and it receives
job.finished with the audit's job_id. An agent that only has a key should poll.
Step 6: results. export.json is prepared in the background: the first request may answer
202 {"status": "pending", "retry_after": 5} with a Retry-After header. Wait that many seconds and repeat
the GET.
curl -s "$ASKANIELA_URL/v1/audits/aud_2fptzm5h3hiruh6c/export.json" -H "Authorization: Bearer $ASKANIELA_API_KEY"
Step 7: fix plan in Markdown, ready to hand to a person or another agent. Same 202 + Retry-After on the
first request; later requests read the stored plan for free.
curl -s "$ASKANIELA_URL/v1/audits/aud_2fptzm5h3hiruh6c/fix-plan?format=md" -H "Authorization: Bearer $ASKANIELA_API_KEY"
# # Fix plan: example.com
# Source: site audit aud_2fptzm5h3hiruh6c · Generated: 2026-10-20 20:12 UTC
Also available: export.zip (one Markdown file per page plus audit.json), export.csv, export.html,
export.pdf, and fix-plan?format=json.
3. Search intent for a phrase¶
curl -s "$ASKANIELA_URL/v1/intent?wait=60" \
-H "Authorization: Bearer $ASKANIELA_API_KEY" -H "Content-Type: application/json" \
-d '{"query": "best running shoes", "market": "en-US"}'
# 202 {"id": "itr_…", "status": "running", "tier": "basic", "price_usd": 0.0015, "report": null, …}
- The answer is
202withstatusqueued,running,doneorfailed.?wait=N(up to 60) waits for the result; if it is not ready, pollGET /v1/intent/{id}every 15 seconds untildoneorfailed. price_usdis the charge for this phrase. It is lower when recent search results are reused (freshness: auto, the default);freshalways fetches new results at full price.tier:basic(search intent and the best form of page),premium(Ani also reads the top pages) orcompare(your page incompare_urlnext to them). Prices:GET /v1/tiers(intent[]).- Many phrases:
POST /v1/intent/batch(JSONitems) orPOST /v1/intent/batch/csv.
Costs and limits¶
- Price before ordering: site audits have
POST /v1/audits/site/estimate; page and intent prices are inGET /v1/tiersand in Balance and pricing. - Cost of a call: every paid answer has the header
X-Cost-USD(for example0.0665for one page with the full catalogue,0.0315for spam only). For audits and jobs it is the amount reserved at the start; pages that were not judged are refunded later. - Balance:
GET /v1/usageshows what was used this day and month, the limit and the balance in USD. - Limit reached:
429 quota_exceededwithdetails(limit_usd,used_usd,requested_usd,remaining_usd). The API does not use402; a missing balance is always429 quota_exceeded. Stop and tell the person; retrying will not help. - Too many requests:
429 rate_limitedwithRetry-AfterandRateLimit-*headers. Wait and retry. - Active work: up to 2 audits or jobs run at once per organization (
429 too_many_active_jobs). Wait for one to finish.
Errors¶
Every error from /v1/* has one shape. Branch on code, never on message.
{"error": {"code": "unknown_rule", "message": "Unknown rules: NOPE",
"details": {"rules": ["NOPE"]}, "request_id": "37da8d48f0e74f579f984e1e1cf215e1"}}
message is in English; with the header Accept-Language: pl it is in Polish. code never changes.
| HTTP | code |
What the agent does |
|---|---|---|
| 400 | unknown_rule, rule_disabled, invalid_url, invalid_webhook_url |
fix the input; do not retry as is |
| 401 | unauthorized, invalid_api_key, api_key_expired |
stop and ask the person for a valid key |
| 403 | insufficient_scope |
stop; the person creates a key with details.required_scope |
| 403 | ip_not_allowed, org_suspended, forbidden |
stop and tell the person |
| 404 | report_not_found, audit_not_found, job_not_found, intent_not_found |
check the id (rep_, aud_, job_, itr_) |
| 409 | job_not_controllable, audit_running, upgrade_unavailable |
the state does not allow this action; read the status |
| 413 | batch_too_large, audit_too_large, item_too_large |
split the work, or use a site audit instead of a batch |
| 422 | validation_error |
fix the body; details[] lists the fields |
| 422 | sitemap_not_found, empty_selection, preview_expired, invalid_scope |
change the source or scope; run preview again |
| 429 | rate_limited, too_many_active_jobs |
wait Retry-After seconds, then retry |
| 429 | quota_exceeded |
stop; the limit or balance is used up |
| 502 | fetch_failed |
the page could not be fetched; tell the person, do not loop |
| 502 | inference_failed |
no rule could be scored, nothing charged; retry once later |
| 503 | spend_cap_reached, beta_budget_reached, queue_unavailable, unavailable |
wait Retry-After (often 3600 s) |
| 503 | fix_plan_unavailable, export_unavailable |
nothing charged; retry the GET later |
A 502 or 504 without a JSON body comes from the gateway, for example during a deploy. For a GET, retry
after 30 seconds. For a POST, do not retry blindly (next section). Every answer has X-Request-ID; quote it
when you report a problem. All codes: Errors.
Retrying safely¶
The API has no Idempotency-Key yet, so a repeated POST is new work and a new charge.
- GET can always be repeated.
POST /v1/judge: a repeat checks and charges the page again. After a timeout, look for the report inGET /v1/reports?url=…&limit=1first.POST /v1/audits/site,POST /v1/jobs: after a timeout or a gateway error, checkGET /v1/audits?limit=5orGET /v1/jobs?limit=5. The audit is usually already there.POST /v1/intent: checkGET /v1/intent?q=<phrase>&limit=5. A phrase stuck inrunningis closed by the system asfailedwith a refund after about 40 minutes; do not order it again before that.- Send your own
X-Request-ID([A-Za-z0-9._-]{1,128}) to match requests with your logs.
Reading the results¶
One page (POST /v1/judge, GET /v1/reports/{id})¶
| Field | Meaning |
|---|---|
violations[] |
spam-policy rules |
quality_signals[] |
page quality and helpful-content rules |
positives[], labels[], choices[] |
positive signals, context labels (for example YMYL topic) and rating scales |
not_assessed[] |
policies that cannot be judged from one page, with reason |
skipped[] |
rule ids not run for the chosen modes or rules |
fetch, extraction_ok, truncated |
how the page was read; extraction_ok: false means little main text was found |
Each rule has rule_id, policy, layer, kind, ok (false = the rule could not be scored, see error) and
score_raw from 0 to 1. Signal strength = round(score_raw * 100), from 0 to 100: 80 or more is strong,
50-79 medium, below 50 low. Show it as "signal 87 of 100". It is not a percentage, not a probability and not a
Google decision. With explain: true, hits get evidence.quotes[] (text, verified: the quote was found in
the page by code) and evidence.fix. A hit with unsupported: true has no verified quote and needs a human check.
Ignore the internal fields z, coverage, top1_other, probability, entropy, samples, spread and
prompt_sha; they may change.
Site audit (GET /v1/audits/{id}/export.json, schema_version 1.1)¶
Only confirmed findings are included. Example shape (one finding, shortened):
{
"schema_version": "1.1",
"audit": {"id": "aud_…", "status": "done", "pages_total": 5, "pages_checked": 5, "pages_failed": 0,
"pages_with_findings": 1, "site_levels": {"spam": "high", "quality": "low", "helpful": "low"},
"note": "1 of 5 pages (20%) have a confirmed spam finding. …"},
"pages": [{
"report_id": "rep_…", "url": "https://example.com/best-deals", "pattern": "/<slug>", "risk": "high",
"findings": 1, "spam_level": "high", "quality_level": "low", "helpful_level": "na",
"items": [{
"category": "spam", "rule_id": "SP-AFF", "policy": "Thin affiliation", "title": "…", "level": "high",
"signal": 91, "evidence": [{"text": "…quoted from the page…", "verified": true}],
"fix": "…what to change on this page…", "fix_personal": true,
"sources": [{"label": "…", "url": "https://developers.google.com/search/docs/essentials/spam-policies#thin-affiliate-pages"}]
}]
}]
}
site_levelscome from the share of pages with a confirmed finding: under 2% low, 2-10% medium, over 10% high.- Page levels are
high,medium,low,na(criteria do not apply, for example a utility page) orunknown. riskis the highest confirmed level of the page:high,mediumornone.signalis already 0-100.fix_personal: truemeans the fix was written for this page, otherwise it is the general advice of the rule.sourcesare the official Google guidelines behind the rule.
Python example¶
Requires httpx (pip install httpx). It checks one page, prices an audit and shows where to start and follow it.
"""AskAniela API from Python (httpx): one page, a site audit, results and costs."""
import os
import time
import httpx
API = os.environ.get("ASKANIELA_URL", "https://api.askaniela.com")
KEY = os.environ["ASKANIELA_API_KEY"]
FINAL = {"done", "failed", "cancelled"}
client = httpx.Client(
base_url=API,
headers={"Authorization": f"Bearer {KEY}"},
timeout=httpx.Timeout(15.0, read=300.0), # one page takes up to a few minutes
)
class AnielaError(Exception):
def __init__(self, status: int, code: str, message: str, retry_after: str | None, request_id: str | None):
super().__init__(f"{status} {code}: {message} (request_id={request_id})")
self.status, self.code, self.retry_after, self.request_id = status, code, retry_after, request_id
def call(method: str, path: str, **kw) -> httpx.Response:
r = client.request(method, path, **kw)
if r.status_code >= 400:
is_json = r.headers.get("content-type", "").startswith("application/json")
err = r.json().get("error", {}) if is_json else {} # a 502/504 from a proxy has no JSON body
raise AnielaError(r.status_code, err.get("code", f"http_{r.status_code}"), err.get("message", r.text[:200]),
r.headers.get("Retry-After"), err.get("request_id"))
return r
def cost(r: httpx.Response) -> float:
return float(r.headers.get("X-Cost-USD", "0"))
def wait_ready(path: str, params: dict | None = None, max_wait: int = 900) -> httpx.Response:
"""GET that may answer 202 + Retry-After while a file is prepared. Repeating a GET is safe."""
deadline = time.monotonic() + max_wait
while True:
r = call("GET", path, params=params)
if r.status_code != 202:
return r
if time.monotonic() > deadline:
raise TimeoutError(path)
time.sleep(int(r.headers.get("Retry-After", "10")))
def judge_page(url: str) -> tuple[dict, float]:
r = call("POST", "/v1/judge", json={"url": url, "modes": ["spam"]})
return r.json(), cost(r)
def estimate_site_audit(site: str, pages: int) -> dict:
body = {"site": site, "pages": pages}
preview = call("POST", "/v1/audits/site/preview", json=body).json()
body["preview_id"] = preview["preview_id"]
return {"body": body, "estimate": call("POST", "/v1/audits/site/estimate", json=body).json()}
def start_site_audit(body: dict) -> str:
"""Paid. Call once, after the user accepted the estimate. Never retry this POST blindly."""
return call("POST", "/v1/audits/site", json=body).json()["audit_id"]
def follow_audit(audit_id: str, every: int = 20) -> tuple[dict, str]:
while True:
a = call("GET", f"/v1/audits/{audit_id}").json()
p = a["progress"]
print(f"{p['status']}: {p['done']}/{p['total']} pages, failed {p['failed']}")
if p["status"] in FINAL:
break
time.sleep(every)
export = wait_ready(f"/v1/audits/{audit_id}/export.json").json()
plan = wait_ready(f"/v1/audits/{audit_id}/fix-plan", params={"format": "md"}).text
return export, plan
def strongest(report: dict) -> list[tuple[str, int]]:
"""Spam and quality rules with signal strength 0-100 (score x 100), strongest first."""
rows = [(r["rule_id"], round(r["score_raw"] * 100))
for key in ("violations", "quality_signals") for r in report[key] if r.get("ok")]
return sorted(rows, key=lambda x: -x[1])
if __name__ == "__main__":
print(call("GET", "/v1/usage").json()["org"]["month"])
report, usd = judge_page("https://example.com/")
print("cost", usd, "top rules", strongest(report)[:3])
plan = estimate_site_audit("example.com", pages=5)
print("estimate", plan["estimate"]["cost_usd"], "USD,", plan["estimate"]["seconds"], "s")
# audit_id = start_site_audit(plan["body"]) # only after the user said yes
# export, fix_plan_md = follow_audit(audit_id)
Example system prompt¶
Paste this into the agent's instructions and adjust the limits.
You can use the AskAniela API at https://api.askaniela.com to check web pages against Google spam policies
and page-quality guidelines. The API key is in the environment variable ASKANIELA_API_KEY; send it as
"Authorization: Bearer <key>". Never print, log or repeat the key.
Rules:
1. Check only sites the user owns or is authorised to analyse.
2. Before any paid call, get the price: POST /v1/audits/site/estimate for site audits, GET /v1/tiers for single
pages and search intent. Tell the user the price and wait for an explicit yes. Never start a site audit on
your own. Single pages up to $0.10 in total per conversation do not need a separate yes.
3. Never repeat a POST that creates work (judge, audits/site, jobs, intent) without checking first whether the
first one went through (GET /v1/reports?url=…, /v1/audits?limit=5, /v1/intent?q=…). A repeat is charged again.
4. Repeating GET requests is safe. On 202 wait for Retry-After seconds. Poll audit status every 20-30 seconds.
5. Branch on error.code, never on the message. Stop and ask the user on 401, 403, quota_exceeded and fetch_failed.
Wait Retry-After on rate_limited, too_many_active_jobs and 503 codes.
6. Report results as signal strength from 0 to 100 (score_raw x 100, or "signal" in exports): "signal 87 of 100".
Never present it as a percentage, a probability or a Google penalty. Say "may break the policy" or
"possible quality issue", not "violates".
7. Quote evidence only when verified is true. Use the fix plan (GET /v1/audits/{id}/fix-plan?format=md) for
recommendations instead of inventing your own.
8. After every paid call, read the X-Cost-USD header and keep a running total for the user.
Docs for agents: https://docs.askaniela.com/llms.txt and https://docs.askaniela.com/user/agents.md
What llms.txt and openapi.json tell agents¶
| Address | What it gives |
|---|---|
https://docs.askaniela.com/llms.txt |
index of all docs pages as Markdown links with one-line summaries (llms.txt format) |
https://docs.askaniela.com/llms-full.txt |
all docs pages in one Markdown file |
https://docs.askaniela.com/<page>.md |
any docs page as plain Markdown, for example https://docs.askaniela.com/user/agents.md; Polish pages under /pl/ |
https://api.askaniela.com/llms.txt |
short index on the API host: docs links and the OpenAPI address |
https://api.askaniela.com/openapi.json |
live OpenAPI 3.1 description of the API |
Known gaps in openapi.json, which this page fills in:
- It does not declare the
Bearer gsj_...key yet; it lists a session cookie and a password login. Agents and tool generators should ignore those and sendAuthorization: Bearer gsj_.... - It has no request examples, errors are described only as
4XX/5XX, and theX-Cost-USDandRetry-Afterheaders are not declared. Use the Errors table and the steps above. - It lists account, session and admin operations too. Expose to an agent only the operations from the map of tasks.
Next: API quickstart, API keys, Site audits guide, Webhooks and jobs, Errors.