API v1
Revscale GEO API
The same engine behind the dashboard. Submit an entity, receive a signed webhook when the analysis completes, and fetch a normalized result with full model and grounding provenance. Your application owns presentation; the API owns the intelligence.
1. Resolve, then confirm
Resolution is free. It enriches the entity, proposes competitors and a query universe, and quotes credits per depth. Confirm or edit before creating a run. For people, resolve first: an ambiguous name is rejected by the one-shot path.
POST /api/v1/subjects/resolve # free, synchronous
{ "name": "Revscale", "url": "revscale.com" }
200 OK
{ "resolution": { "subject": { … }, "competitors": [ … ], "alternatives": [ … ], "confidence": 0.9 },
"proposedPrompts": [ { "text": "…", "intent": "discovery", "branded": false, "mentionsCompetitor": false, … } ],
"depthOptions": [ { "preset": "standard", "prompts": 12, "samples": 1, "credits": 12, "executions": 48 } ],
"providers": [ { "id": "openai", "available": true, "modelId": "gpt-5.4", "search": "tool" } ] }
# Edit anything, then POST /api/v1/analyses with { subject, competitors, prompts, depth, providers }.2. Create an analysis
Asynchronous. Returns 202 with the run id. Processing starts immediately and survives function timeouts; call POST /api/v1/analyses/:id/process to resume a stalled run, or schedule /api/v1/maintenance/resume with your CRON_SECRET.
POST /api/v1/analyses
Authorization: Bearer rgeo_...
Idempotency-Key: 5c1f... # optional; same key + same body returns the same run
Content-Type: application/json
{
"autoResolve": true,
"kind": "location", # organization | location | person (inferred when omitted)
"name": "Signarama Boca Raton",
"url": "signarama.com/boca-raton",
"market": { "city": "Boca Raton", "region": "FL" },
"category": "sign shop",
"competitors": ["FastSigns Boca Raton"], # optional; we discover the rest
"depth": "standard", # quick | standard | deep, or { prompts, samples, grounding }
"providers": ["openai", "anthropic", "google", "perplexity"],
"webhookUrl": "https://example.com/hooks/geo"
}
202 Accepted
{ "analysis": { "id": "…", "status": "queued", … }, "webhookSecret": "whsec_…", "links": { "self": "/api/v1/analyses/…" } }3. Receive the result
POST <webhookUrl>
X-Revscale-Event: analysis.completed # analysis.partial | analysis.failed
X-Revscale-Signature: t=1725500000,v1=<hex HMAC-SHA256 of "${t}.${body}" using webhookSecret>
{ "event": "analysis.completed", "analysisId": "…", "status": "completed",
"resultUrl": "https://…/api/v1/analyses/…",
"summary": { "visibilityIndex": 61.4, "mentionRate": 0.58, "shareOfVoice": 0.31, "topGap": "…", "credits": { … } } }
# Retries: 1m, 5m, 30m, 2h, 12h. Verify the signature, then GET resultUrl.GET /api/v1/analyses/:id
{
"status": "completed" | "partial" | "failed" | "running" | "queued",
"subject": { … }, "competitors": [ … ],
"providers": [ { "id": "openai", "modelId": "gpt-5.4", "modelSnapshots": ["gpt-5.4-2026-03-05"], "succeeded": 12, "failed": 0, "groundedRate": 0.92 } ],
"prompts": [ { "id": "…", "text": "…", "intent": "local", "branded": false } ],
"responses": [ { "providerId": "…", "groundingMode": "native_web_search", "citations": [ … ], "text": "…", "extraction": { "subjectMentioned": true, "subjectPosition": 2, … } } ],
"scoring": {
"scoringVersion": 2,
"overall": { "visibilityIndex": 61.4, "formula": "…", "weights": { … }, "components": { … } },
"subject": { "mentionRate": 0.58, "shareOfVoice": 0.31, "averagePosition": 2.4, "top3Rate": 0.8, … },
"competitors": [ … ], "providers": [ … ], "prompts": [ … ], "sources": [ { "domain": "yelp.com", "citations": 9, … } ],
"gaps": [ { "kind": "competitor", "title": "…", "detail": "…", "severity": 0.33, "evidence": { … } } ],
"strongestProvider": "openai", "weakestProvider": "perplexity"
},
"credits": { "reserved": 12, "settled": 12 },
"provenance": { "scoringVersion": 2, "extractorVersion": "v2.0.0", "lane": "current" }
}Guarantees
- The answering model receives the query only. It is never told it is being measured, never asked to rank, and never given competitor names on discovery queries.
groundingModeis derived from observed search activity per response:native_web_search,grounded_search,search_failedormodel_knowledge_only.- Provider failures produce a
partialrun with per-provider detail. Metrics use valid observations only and missing values are null, never zero. - Credits: one per query per sample, settled once on completion for query runs with at least one valid answer. Retrying with the same
Idempotency-Keynever creates a second run. - Every execution records provider, model id, provider-reported model snapshot, lane, grounding mode, prompt set and timestamp, so historical change can be separated from instrument change.
Rate limits and scale
Runs execute with per-provider concurrency limits and retries. For high-volume or multi-location programs, contact Revscale for dedicated capacity.