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,signalsimport 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}
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.
| Param | Type | Default | Description |
|---|---|---|---|
sections | string | all | Comma-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
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}
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:
| Header | Example |
|---|---|
webhook-id | UUID v4 (also the job_id) |
webhook-timestamp | Unix seconds |
webhook-signature | v1,<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 Falseimport 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
| HTTP | code | Cause |
|---|---|---|
| 400 | no_sections | ?sections= didn't include valid keys |
| 400 | callback_url_insecure | callback_url must be https:// |
| 400 | callback_secret_too_short | Min 16 bytes |
| 401 | missing_token / invalid_token | Bearer missing or revoked |
| 404 | job_not_found | Async job not yet recorded — keep polling |
| 413 | sync_too_large | Report >256KB; use /refresh with callback_url |
| 429 | rate_limited | Per-key minute window exhausted |
| 429 | quota_exceeded | Monthly quota exhausted (cached + fresh combined) |
| 429 | fresh_quota_exceeded | Monthly fresh-refresh quota exhausted — cached still works |
Tier quotas
| Tier | Price | Fresh/mo | Cached/mo | Req/min | Domains |
|---|---|---|---|---|---|
| Free | $0 | 3 | 100 | 30 | 1 |
| Starter | $39 | 30 | 2,000 | 60 | 5 |
| Agency | $149 | 300 | 20,000 | 300 | 50 |
Upgrade from your dashboard at /agency/api-keys. Tier applies to every key in your agency.