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.
  • groundingMode is derived from observed search activity per response: native_web_search, grounded_search, search_failed or model_knowledge_only.
  • Provider failures produce a partial run 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-Key never 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.