AEONiti API — Quickstart

Two API calls take you from a bare domain to the full AEO intelligence stack. The first registers the domain and kicks off the onboarding pipeline (vertical detect, brand inference, CC crawl, Ollagraph 12-endpoint signals, AEO probes across the standard 3 LLMs). The second pulls the bundled report. Dashboard at login.aeoniti.com is read-only — the API is where configuration lives.

1. Get an API key

Sign in to login.aeoniti.com and go to /agency/api-keys. Click Create key. The plaintext token is shown once — save it.

2. Onboard a domain (POST /v1/domains)

Two modes — pick the one that fits your stack:

2a. Recommended — bring your own prompts

The cross-tenant probe cache (under the hood: shared by all agencies hitting the same prompt × LLM × day) hits when prompts match exactly. Your AI agent knows your client's business better than our generic vertical-detect can infer it — supply custom_prompts in the body and you skip our prompt-gen LLM call (saving ~$0.0003 per onboard) and become eligible for the cross-tenant cache (the dominant gross-margin lever — first agency to probe a prompt pays full LLM cost, subsequent agencies probing the same prompt on the same day pay $0 marginal).

# 1. Recommended: register + supply YOUR prompts
#    Your AI agent knows your client's business better than our
#    vertical-detect can infer. Identical prompts across agencies
#    hit the cross-tenant probe cache for $0 marginal LLM cost.
curl -X POST https://api.aeoniti.com/v1/domains \
     -H "Authorization: Bearer aeo_..." \
     -d '{
       "domain": "acme.com",
       "custom_prompts": [
         "best CRM for indian startups in 2026",
         "Acme vs HubSpot for mid-market SaaS",
         "how do I migrate from Salesforce to a modern CRM",
         "top CRM tools with built-in email automation",
         "I need a CRM that supports Hindi UI"
       ]
     }'
# → 202 { "domain_id": 171, "job_id": "...",
#         "status_url": "/v1/reports/171", "eta_seconds": 75, "created": true }

3–10 prompts recommended. Fewer reduces signal; more wastes quota since dashboard tiles surface the top 5 by mention rate. Identical prompt strings across agencies hit cache — case & surrounding whitespace are normalised.

2b. Auto-everything — domain only

Skip custom_prompts and we generate 5 buyer-intent prompts via the vertical playbook + brand inference. Slower (extra LLM call), more expensive (no cache hits expected because our prompt-gen is non-deterministic), but works for callers who don't have their own prompt-gen.

# 2. Auto-everything: register, we generate prompts
#    Falls back to our vertical-detect + brand-inference pipeline.
#    Works without a body beyond the domain, but the prompts will be
#    LLM-generated and non-deterministic — cache hits across agencies
#    are unlikely.
curl -X POST https://api.aeoniti.com/v1/domains \
     -H "Authorization: Bearer aeo_..." \
     -d '{"domain":"acme.com"}'
# → 202 { "domain_id": 171, "job_id": "...",
#         "status_url": "/v1/reports/171", "eta_seconds": 75, "created": true }

3. Pull the bundled report (GET /v1/reports/{id})

Poll the status_url after ~75s (cold) or instantly if cached within the last 24h. The response packs every section into one JSON.

# 2. Pull the bundled report (poll after ~75s, or wire a webhook)
curl -H "Authorization: Bearer aeo_..." \
     https://api.aeoniti.com/v1/reports/170?sections=overview,readiness,signals
import requests
r = requests.get(
    "https://api.aeoniti.com/v1/reports/170",
    params={"sections": "overview,readiness,signals"},
    headers={"Authorization": "Bearer aeo_..."},
    timeout=30,
)
report = r.json()
print(report["sections"]["readiness"]["headline_score"])
const res = await fetch(
  "https://api.aeoniti.com/v1/reports/170?sections=overview,readiness,signals",
  { headers: { "Authorization": "Bearer aeo_..." } }
);
const report = await res.json();
console.log(report.sections.readiness.headline_score);

Authentication

Bearer token in the Authorization header on every request:

Authorization: Bearer aeo_a1b2c3d4...

Tokens have format aeo_ + 56 hex chars. Generated by signing in and going to /agency/api-keys. We only store the SHA-256 hash — if you lose the plaintext, mint a new one (and revoke the old).

GET /v1/reports/{domain_id}

GET/v1/reports/{domain_id}?sections=a,b,c

Returns the cached report if one exists within the last 24h. Otherwise generates fresh + persists + returns. Cached fetches cost zero LLM tokens and don't decrement monthly_fresh_quota.

ParamTypeDefaultDescription
sectionsstringallComma-separated. Valid: overview, citations, readiness, signals, recommendations.

Response shape

{
  "meta": {
    "domain_id": 170,
    "domain": "networkershome.com",
    "version_id": "uuid",
    "generated_at": "2026-05-28T12:41:10Z",
    "cached": true,
    "source": "aeoniti.v1"
  },
  "sections": {
    "overview": { ... },
    "readiness": { "headline_score": 65, "headline_grade": "C", ... },
    "signals": { "cards": [ ... ] }
  }
}

Each section is omitted entirely if not requested or if its fetch errored. See the OpenAPI spec for full per-section schemas.

POST /v1/reports/{domain_id}/refresh

POST/v1/reports/{domain_id}/refresh

Async. Returns 202 immediately. We build the report in the background then POST the signed payload to your callback_url.

curl -X POST \
  -H "Authorization: Bearer aeo_..." \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://yourapp.com/aeoniti-webhook",
    "callback_secret": "your-32-byte-secret-here",
    "sections": "overview,readiness"
  }' \
  https://api.aeoniti.com/v1/reports/170/refresh

GET /v1/reports/{domain_id}/refresh/{job_id}

GET/v1/reports/{domain_id}/refresh/{job_id}

Returns 404 until the webhook has been attempted, then 200 with the delivery outcome:

{
  "job_id": "uuid",
  "succeeded": true,
  "attempt_n": 1,
  "response_status": 200,
  "error_kind": null,
  "sent_at": "2026-05-28T12:42:00Z"
}

Verifying webhook signatures

We sign every webhook with HMAC-SHA256 per the Standard Webhooks spec. Headers sent:

HeaderExample
webhook-idUUID v4 (also the job_id)
webhook-timestampUnix seconds
webhook-signaturev1,<base64-hmac-sha256>

Signed string: {webhook-id}.{webhook-timestamp}.{raw-body}. Reject events older than 5 minutes to prevent replay.

import hmac, hashlib, base64, time

def verify(secret: bytes, headers: dict, raw_body: bytes) -> bool:
    wid = headers["webhook-id"]
    ts  = int(headers["webhook-timestamp"])
    if abs(time.time() - ts) > 300:
        return False
    signed = f"{wid}.{ts}.".encode() + raw_body
    digest = hmac.new(secret, signed, hashlib.sha256).digest()
    expected = "v1," + base64.b64encode(digest).decode()
    for sig in headers["webhook-signature"].split(" "):
        if hmac.compare_digest(sig, expected):
            return True
    return False
import crypto from "node:crypto";

function verify(secret, headers, rawBody) {
  const wid = headers["webhook-id"];
  const ts = parseInt(headers["webhook-timestamp"], 10);
  if (Math.abs(Date.now()/1000 - ts) > 300) return false;
  const signed = `${wid}.${ts}.${rawBody}`;
  const digest = crypto.createHmac("sha256", secret).update(signed).digest("base64");
  const expected = `v1,${digest}`;
  return headers["webhook-signature"].split(" ").some(sig =>
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
  );
}
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "strconv"
    "strings"
    "time"
)

func verify(secret []byte, headers map[string]string, body []byte) bool {
    wid := headers["webhook-id"]
    ts, _ := strconv.ParseInt(headers["webhook-timestamp"], 10, 64)
    if abs(time.Now().Unix()-ts) > 300 { return false }
    signed := []byte(wid + "." + headers["webhook-timestamp"] + ".")
    signed = append(signed, body...)
    h := hmac.New(sha256.New, secret)
    h.Write(signed)
    expected := "v1," + base64.StdEncoding.EncodeToString(h.Sum(nil))
    for _, sig := range strings.Split(headers["webhook-signature"], " ") {
        if hmac.Equal([]byte(sig), []byte(expected)) { return true }
    }
    return false
}

Error codes

HTTPcodeCause
400no_sections?sections= didn't include valid keys
400callback_url_insecurecallback_url must be https://
400callback_secret_too_shortMin 16 bytes
401missing_token / invalid_tokenBearer missing or revoked
404job_not_foundAsync job not yet recorded — keep polling
413sync_too_largeReport >256KB; use /refresh with callback_url
429rate_limitedPer-key minute window exhausted
429quota_exceededMonthly quota exhausted (cached + fresh combined)
429fresh_quota_exceededMonthly fresh-refresh quota exhausted — cached still works

Tier quotas

TierPriceFresh/moCached/moReq/minDomains
Free$03100301
Starter$39302,000605
Agency$14930020,00030050

Upgrade from your dashboard at /agency/api-keys. Tier applies to every key in your agency.