Authentication
Every request needs your API key in the Authorization header, as a bearer token:
Authorization: Bearer sk_live_your_api_key_here
Get a key by signing up: a free trial key (20 requests, no card required) is issued instantly from your dashboard. Missing or invalid keys return UNAUTHORIZED with a 401 status.
Quota & rate limits
Each plan has a monthly request quota shared across both endpoints. An audit costs 1 request; a fix generation costs 5 requests (or 1 if the site's content hasn't changed since your last call, see previousContentHash below). Exceeding your quota returns QUOTA_EXCEEDED with a 429 status and these headers:
| Header | Meaning |
|---|---|
X-Quota-Limit | Your plan's monthly quota |
X-Quota-Remaining | Requests left this period (never negative) |
Retry-After | Paid plans only: seconds until it's worth trying again |
X-Quota-Reset | Paid plans only: approximate ISO timestamp the quota resets |
Trial keys never reset automatically. Upgrade from your dashboard for a monthly quota instead.
POST /v1/audit
Crawls a URL the same way an AI crawler would and scores it across four categories, with evidence for every check. Never calls an LLM: artifacts in the response is always empty; use /v1/generate to actually produce fix content.
Request
{
"url": "https://example.com"
}Response: 200
{
"url": "https://example.com/",
"auditedAt": "2026-08-06T12:00:00.000Z",
"engineVersion": "1.0.0",
"score": {
"total": 58,
"band": "partially-visible",
"categories": { "crawlAccess": 18, "structuredData": 9, "answerReadiness": 17, "napConsistency": 14 }
},
"checks": [
{
"id": "schema-localbusiness-missing",
"category": "structuredData",
"status": "fail",
"weight": 10,
"evidence": "No JSON-LD of type LocalBusiness found on /",
"fix": { "summary": "Add a LocalBusiness JSON-LD block to the homepage <head>", "priority": 1, "effort": "low", "generatedArtifact": "jsonld" }
}
],
"artifacts": { "jsonld": "", "llmsTxt": "", "faqSuggestions": [], "needsConfirmation": [] },
"meta": { "pagesCrawled": 5, "usedBrowserRendering": false, "needsBrowserRendering": false, "contentHash": "abc123" }
}score.band is one of ai-ready, partially-visible, or likely-invisible. checks[].status is pass, warn, or fail. fix is present on any check that isn't a clean pass.
Errors
| Code | HTTP status | Meaning | Headers |
|---|---|---|---|
INVALID_URL | 400 | Request body is missing "url", or it isn't a valid http/https URL. | None |
UNAUTHORIZED | 401 | Missing, malformed, revoked, or unknown API key. | None |
QUOTA_EXCEEDED | 429 | This key has used its available quota for the current billing period. | X-Quota-Limit, X-Quota-Remaining, Retry-After*, X-Quota-Reset* |
SITE_UNREACHABLE | 502 | The site couldn't be fetched or crawled (DNS failure, timeout, connection refused, etc.). | None |
* Retry-After and X-Quota-Reset are only sent for paid plans; trial keys have no reset date.
Examples
curl https://api.geogize.ai/v1/audit \
-X POST \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'const res = await fetch("https://api.geogize.ai/v1/audit", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://example.com" }),
});
const audit = await res.json();POST /v1/generate
Runs the same crawl and checks as /v1/audit, then calls the LLM to generate real fix content (JSON-LD, an llms.txt file, FAQ suggestions) using only facts extracted from the site. Anything it can't confirm is listed in needsConfirmation rather than invented.
Request
{
"url": "https://example.com",
"previousContentHash": "abc123"
}previousContentHash is optional: pass the contentHash from a previous /v1/audit or /v1/generate call for this URL. If the site's content hasn't changed, generation is skipped (costing 1 request instead of 5) and you get back the same artifacts you'd expect from your last call, unchanged.
Response: 200 (generated)
{
"skipped": false,
"contentHash": "abc123",
"artifacts": {
"jsonld": "{ \"@context\": \"https://schema.org\", \"@type\": \"LocalBusiness\", ... }",
"llmsTxt": "# Example Ltd\n\n> A plain-English summary of what this business does...",
"faqSuggestions": ["What areas do you serve? ..."],
"needsConfirmation": ["Confirm your business phone number, not found on the site."]
}
}Response: 200 (skipped, content unchanged)
{
"skipped": true,
"contentHash": "abc123"
}Errors
| Code | HTTP status | Meaning | Headers |
|---|---|---|---|
INVALID_URL | 400 | Request body is missing "url", or it isn't a valid http/https URL. | None |
UNAUTHORIZED | 401 | Missing, malformed, revoked, or unknown API key. | None |
RATE_LIMITED | 429 | Too many generation requests from this address in a short window. | Retry-After |
QUOTA_EXCEEDED | 429 | This key has used its available quota for the current billing period. | X-Quota-Limit, X-Quota-Remaining, Retry-After*, X-Quota-Reset* |
SITE_UNREACHABLE | 502 | The site couldn't be fetched or crawled. | None |
GENERATION_FAILED | 502 | The LLM call failed or returned unusable output. Safe to retry. | None |
* Retry-After and X-Quota-Reset are only sent for paid plans on QUOTA_EXCEEDED; trial keys have no reset date.
Examples
curl https://api.geogize.ai/v1/generate \
-X POST \
-H "Authorization: Bearer sk_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'const res = await fetch("https://api.geogize.ai/v1/generate", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://example.com" }),
});
const result = await res.json();
if (!result.skipped) {
console.log(result.artifacts.jsonld);
}MCP server
Prefer to work from Claude Desktop, Claude Code, or any other MCP client instead of raw HTTP? The engine exposes the same audit and fix-generation capability as an MCP server over Streamable HTTP, authenticated with the exact same API key as the REST endpoints above.
Send your key the same way as REST: as a Bearer token in the Authorization header. There's no separate MCP-specific auth or key type. A missing or invalid key returns MCP_AUTH_FAILED with a 401 status before any tool call is even attempted.
Client config
Most MCP clients take a JSON config listing their servers. This is the standard shape for a remote server on the MCP specification's Streamable HTTP transport:
{
"mcpServers": {
"geogize": {
"type": "http",
"url": "https://api.geogize.ai/mcp",
"headers": {
"Authorization": "Bearer sk_live_your_api_key_here"
}
}
}
}Exact key names vary between clients: some spell the transport "type": "streamableHttp" or "transport": "http", and some add remote servers through their own UI rather than a config file. Check your client's own documentation, and note we haven't yet live-verified this snippet against a specific client. The endpoint itself is plain Streamable HTTP with a bearer token, so any spec-compliant client can reach it.
Tools
| Tool | Input | Output | Cost |
|---|---|---|---|
audit_website | { url }: a full URL including scheme. | The same score, category breakdown, and pass/warn/fail checks (each with a fix) as POST /v1/audit. | 1 request |
generate_fixes | { url, previousContentHash? }: pass previousContentHash from a prior call to skip regeneration when content hasn't changed. | The same generated artifacts (JSON-LD, llms.txt, FAQ suggestions) as POST /v1/generate. | 5 requests (1 if skipped via previousContentHash match) |
Both tools draw from the same monthly quota as the REST endpoints: a call from an MCP client and a call to /v1/audit for the same key count against the same pool. Tool-level errors (invalid URL, quota exceeded, site unreachable, generation failed) come back as a normal tool result with isError: true rather than an HTTP error, so your MCP client can see and act on them.
One difference from REST: a tool result carries no HTTP headers of its own, so a QUOTA_EXCEEDED tool error doesn't come with the X-Quota-Limit, X-Quota-Remaining, Retry-After or X-Quota-Reset headers documented above. The same numbers are included in the error message text instead.