Skip to content

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, html or text. Optional: modes (["spam"], ["quality"], ["helpful"] or a mix; fewer modes cost less), rules (ids from GET /v1/rules), render_js, explain (evidence and fixes for hits), report_lang (en or pl), project_id.
  • The call is synchronous. One page with the full catalogue took about 2 minutes on staging; fewer modes are 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_…), then GET /v1/reports/{id}/fix-plan?format=md.
  • More than one page: POST /v1/judge/batch with {"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 202 with status queued, running, done or failed. ?wait=N (up to 60) waits for the result; if it is not ready, poll GET /v1/intent/{id} every 15 seconds until done or failed.
  • price_usd is the charge for this phrase. It is lower when recent search results are reused (freshness: auto, the default); fresh always fetches new results at full price.
  • tier: basic (search intent and the best form of page), premium (Ani also reads the top pages) or compare (your page in compare_url next to them). Prices: GET /v1/tiers (intent[]).
  • Many phrases: POST /v1/intent/batch (JSON items) or POST /v1/intent/batch/csv.

Costs and limits

  • Price before ordering: site audits have POST /v1/audits/site/estimate; page and intent prices are in GET /v1/tiers and in Balance and pricing.
  • Cost of a call: every paid answer has the header X-Cost-USD (for example 0.0665 for one page with the full catalogue, 0.0315 for 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/usage shows what was used this day and month, the limit and the balance in USD.
  • Limit reached: 429 quota_exceeded with details (limit_usd, used_usd, requested_usd, remaining_usd). The API does not use 402; a missing balance is always 429 quota_exceeded. Stop and tell the person; retrying will not help.
  • Too many requests: 429 rate_limited with Retry-After and RateLimit-* 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 in GET /v1/reports?url=…&limit=1 first.
  • POST /v1/audits/site, POST /v1/jobs: after a timeout or a gateway error, check GET /v1/audits?limit=5 or GET /v1/jobs?limit=5. The audit is usually already there.
  • POST /v1/intent: check GET /v1/intent?q=<phrase>&limit=5. A phrase stuck in running is closed by the system as failed with 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_levels come 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) or unknown.
  • risk is the highest confirmed level of the page: high, medium or none.
  • signal is already 0-100. fix_personal: true means the fix was written for this page, otherwise it is the general advice of the rule. sources are 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 send Authorization: Bearer gsj_....
  • It has no request examples, errors are described only as 4XX/5XX, and the X-Cost-USD and Retry-After headers 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.