Agentic SSO Documentation

Everything you need to integrate GEOstack's Agentic SSO into your application.

Overview

GEOstack Agentic SSO is an OAuth-style authorization layer purpose-built for AI agents. It lets users grant AI agents (Claude, ChatGPT, Gemini, etc.) scoped access to your application through a secure consent flow.

Think of it as "Login with Google" but for AI agents instead of humans.

Base URL Production: https://shadow.geostack.xyz/api/v1
SSO endpoints: /api/v1/sso/...
Dashboard API: /api/v1/sso-dash/...

How It Works

The flow has 4 steps:

1. Your app redirects user → GEOstack consent screen 2. User approves → GEOstack issues a "visa" (token) 3. User is redirected back → your app receives the visa_token 4. Your backend validates the visa → agent gets scoped access

Consent URL Format

https://geostack.xyz/sso?app_id=YOUR_APP_ID&redirect_uri=https://yourapp.com/callback&scopes=read:data,write:data&state=RANDOM_STATE
ParameterRequiredDescription
app_idYesYour application ID from the dashboard
redirect_uriYesMust match the URI registered in your app settings
scopesYesComma-separated list of requested permissions
stateRecommendedRandom string to prevent CSRF. Returned unchanged in callback.

Callback Parameters

After user approval, they're redirected to your redirect_uri with:

ParameterDescription
visa_tokenThe JWT token representing the agent's authorization
visa_idUnique ID of this visa
stateYour original state parameter (verify this matches!)

Authentication

The API supports two authentication methods:

1. API Keys (recommended for backends)

Generate key pairs from the Developer Console. You'll get:

Key TypePrefixUsage
Publishablegeo_pk_live_Safe for frontend. Read-only operations.
Secretgeo_sk_live_Backend only. Full API access.
Test Publishablegeo_pk_test_Sandbox environment.
Test Secretgeo_sk_test_Sandbox environment.
Authorization: Bearer geo_sk_live_xxxxxxxxxxxx
Important Secret keys (geo_sk_) must never be exposed in frontend code. Use publishable keys (geo_pk_) for client-side operations.

2. JWT Token (for dashboard web UI)

Used internally by the dashboard. Obtained via email OTP login at /sso-dashboard.

Agent Integration

After a user approves the consent screen, here's what happens and how the AI agent uses the visa:

What the user receives

Your app's callback URL receives visa_token, visa_id, and state as query parameters. Your backend should store the visa_token.

How the AI agent presents the visa

When an AI agent (Claude, ChatGPT, etc.) wants to access your API on behalf of the user, it includes the visa token:

// Agent sends this to YOUR API: GET https://yourapp.com/api/agent/data Authorization: Bearer <visa_token>

How your backend validates

Your API middleware extracts the visa token and validates it with GEOstack:

// In your API middleware: const visaToken = req.headers.authorization?.replace('Bearer ', ''); const res = await fetch('https://shadow.geostack.xyz/api/v1/sso/validate', { method: 'POST', headers: { 'Authorization': 'Bearer geo_sk_live_YOUR_SECRET_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ visa_token: visaToken }), }); const { data } = await res.json(); if (!data.valid) return res.status(401).json({ error: 'Invalid visa' }); // Check scopes if (!data.scopes.includes('read:data')) { return res.status(403).json({ error: 'Insufficient scope' }); } // Proceed — agent is authorized req.agentUserId = data.user_id; req.agentScopes = data.scopes; next();

Visa Lifetime

SettingDefaultRange
TTL30 days1 – 365 days

The TTL is set at grant time via the expires_in_days parameter on the consent flow. Once expired, the visa is permanently invalid — the user must re-approve through the consent screen.

Caching tip Since visas don't change once issued (they can only be revoked), cache the /validate response for 30-60 seconds. Check expires_at client-side to avoid unnecessary API calls.

Validate App Info

GET /api/v1/sso/consent-info

Validates an app and redirect URI before showing the consent screen. Call this to verify your integration.

Query ParamRequiredDescription
app_idYesYour application ID
redirect_uriNoValidated against registered URI if provided

Response (200)

{ "valid": true, "data": { "app_name": "My SaaS", "domain": "mysaas.com", "description": "Project management for teams", "allowed_scopes": ["read:data", "write:data"], "redirect_uri": "https://mysaas.com/callback" } }

Validate Visa

POST /api/v1/sso/validate

Validate a visa token. Call this from your backend when an AI agent presents a token.

Authentication: Requires geo_sk_ (secret) API key.

Request

{ "visa_token": "eyJhbGciOiJIUzI1NiIs..." }

Response (200)

{ "data": { "valid": true, "visa_id": "uuid-here", "partner_app_id": "app-uuid", "user_id": "user-uuid", "agent_type": "chatgpt", "scopes": ["read:data", "write:data"], "expires_at": "2026-05-14T00:00:00Z" } }
Event fired Each successful validation fires a visa.validated event and webhook.

Revoke Visa

POST /api/v1/sso/revoke

Revoke a visa. The visa will immediately become invalid for all future validation attempts.

Authentication: JWT (user revokes their own visa) or API key (developer revokes on behalf of app).

Request

{ "visa_id": "visa-uuid-to-revoke" }

Response (200)

{ "data": { "visa_id": "visa-uuid", "revoked": true } }
Irreversible Revoking a visa is permanent. The agent will need to go through the consent flow again to get a new visa.

Applications

List Applications

GET /api/v1/sso-dash/apps

Returns all apps owned by the authenticated user (JWT) or the app associated with the API key.

Create Application

POST /api/v1/sso-dash/apps

Authentication: JWT only (not API keys).

Request

{ "app_name": "My SaaS", "domain": "mysaas.com", "redirect_uri": "https://mysaas.com/callback", "description": "Optional description", "allowed_scopes": ["read:data", "write:data"] }

Update Application

PATCH /api/v1/sso-dash/apps/:id

Update one or more fields. Only include the fields you want to change.

Request

{ "app_name": "Updated Name", // optional, 2-100 chars "redirect_uri": "https://new.url", // optional, valid URL "description": "New description", // optional, max 500 chars "logo_url": "https://logo.url", // optional, valid URL "allowed_scopes": ["read:data"] // optional, replaces entire list }

Delete Application

DELETE /api/v1/sso-dash/apps/:id

Soft-deletes (sets status to inactive). Existing visas remain valid until expiry.

Rotate App Secret

POST /api/v1/sso-dash/apps/:id/rotate

Generates a new app secret. The old secret is immediately invalidated.

Visas

List Visas

GET /api/v1/sso-dash/apps/:id/visas

Returns all visas issued for a specific application.

Response

{ "data": [ { "id": "visa-uuid", "user_id": "user-uuid", "agent_type": "claude", "scopes": ["read:data"], "expires_at": "2026-05-14T00:00:00Z", "revoked_at": null, "issued_at": "2026-04-14T12:00:00Z" } ] }

API Keys

Generate Key Pair

POST /api/v1/sso-dash/apps/:id/keys

Request

{ "environment": "live", // or "test" "label": "Production Server" }

Response (201)

{ "data": { "publishable_key": "geo_pk_live_xxxx", "secret_key": "geo_sk_live_xxxx" }, "warning": "Save the secret key now. It will not be shown again." }

List Keys

GET /api/v1/sso-dash/apps/:id/keys

Revoke Key

DELETE /api/v1/sso-dash/keys/:id

Analytics

GET /api/v1/sso-dash/analytics
Query ParamDefaultDescription
period30dTime range: 7d, 30d, or 90d

Response

{ "data": { "period": "30d", "daily_activity": [ { "date": "2026-04-14", "event_type": "visa.issued", "count": 12 } ], "agent_breakdown": [ { "agent_type": "chatgpt", "count": 45 }, { "agent_type": "claude", "count": 23 } ], "top_scopes": [ { "scope": "read:data", "count": 68 } ], "error_rate": { "total": 100, "errors": 3, "rate": 3 }, "api_calls": { "total": 500, "today": 12, "this_week": 89, "this_month": 500 }, "peak_usage": { "hour": "2026-04-14 14:00", "count": 23 } } }

Webhooks

Webhooks notify your server in real-time when events happen in your GEOstack SSO integration.

Create Webhook

POST /api/v1/sso-dash/webhooks

Request

{ "app_id": "your-app-uuid", "url": "https://yourapp.com/webhooks/geostack", "events": ["visa.issued", "visa.revoked", "visa.validated"] }

Response (201)

{ "data": { "id": "webhook-uuid", "secret": "your-signing-secret" }, "warning": "Save this secret now. It will not be shown again." }

Webhook Events

EventFired WhenPayload
visa.issuedA new visa is granted to an AI agentvisa_id, user_id, agent_type, scopes
visa.validatedA visa is validated via /validatevisa_id, scopes, agent_type, validated_at
visa.revokedA visa is revoked by user or developervisa_id, revoked_by
app.registeredA new app is createdapp_name, domain
webhook.deliveredA webhook is successfully deliveredwebhook_id, url, status
webhook.failedA webhook delivery failed (after 3 retries)webhook_id, url, status

Webhook Payload Format

{ "event": "visa.issued", "timestamp": "2026-04-14T12:00:00.000Z", "data": { "visa_id": "uuid", "user_id": "uuid", "agent_type": "claude", "scopes": ["read:data"] } }

Webhook Headers

HeaderDescription
X-GEOstack-Signaturesha256=<hmac_hex> — HMAC-SHA256 of the raw body
X-GEOstack-EventEvent type (e.g., visa.issued)
X-GEOstack-DeliveryUnique delivery ID
User-AgentGEOstack-Webhook/1.0

Signature Verification

Always verify webhook signatures to ensure payloads are from GEOstack.

Node.js

import crypto from 'node:crypto'; const signature = req.headers['x-geostack-signature']; const expected = 'sha256=' + crypto .createHmac('sha256', WEBHOOK_SECRET) .update(JSON.stringify(req.body)) .digest('hex'); if (signature !== expected) { return res.status(401).send('Invalid signature'); }

Python

import hmac, hashlib sig = request.headers.get('X-GEOstack-Signature', '') expected = 'sha256=' + hmac.new( WEBHOOK_SECRET.encode(), request.data, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(sig, expected): return 'Invalid', 401

Error Codes

CodeHTTPDescription
MISSING_APP_ID400app_id parameter is required
APP_NOT_FOUND404Application does not exist
APP_INACTIVE403Application has been deactivated
REDIRECT_MISMATCH400redirect_uri doesn't match the registered URI
AUTH_REQUIRED401No authentication provided (API key or JWT required)
SECRET_KEY_REQUIRED403This endpoint requires a secret key (geo_sk_), not a publishable key
VISA_INVALID401Visa token is invalid, expired, or revoked
SCOPE_DENIED403Requested scope is not allowed for this application
RATE_LIMITED429Too many requests. Retry after the indicated delay.

Error Response Format

{ "error": "Human-readable error message", "code": "ERROR_CODE", "valid": false }

Domain Verification

Verify domain ownership via DNS TXT record. Verified domains display a green badge on the consent screen.

How it works

1. Register your app → GEOstack generates a verify token 2. Add DNS TXT record: geostack-verify=YOUR_TOKEN 3. Call POST /apps/:id/verify-domain 4. GEOstack checks DNS → marks domain as verified

Verify Domain

POST /api/v1/sso-dash/apps/:id/verify-domain

Success Response (200)

{ "data": { "verified": true, "domain": "yourapp.com" } }

Failure Response (400)

{ "error": "DNS TXT record not found", "expected_record": "geostack-verify=abc123...", "instructions": "Add a TXT record to yourapp.com with value: geostack-verify=abc123..." }
DNS propagation TXT record changes can take up to 24-48 hours to propagate globally. If verification fails, wait and try again.

Status API

GET /api/v1/status

Public health check. No authentication required.

Response (200)

{ "status": "operational", "service": "geostack-gateway", "uptime_seconds": 86400, "components": { "database": { "status": "connected", "latency_ms": 3 }, "api": { "status": "operational", "response_ms": 5 } } }

Scopes

Scopes are custom permissions you define for your application. GEOstack does not enforce a fixed taxonomy — you define the scopes that make sense for your API.

Convention

We recommend the action:resource format:

ScopeDescription
read:dataRead access to data
write:dataCreate/update data
delete:dataDelete data
admin:allFull administrative access
Scope validation When a user approves a visa, GEOstack validates that the requested scopes are within the allowed_scopes you registered for your app. If a scope isn't in your allowed list, the visa will be rejected.

Rate Limits

Current rate limits (subject to change):

EndpointLimitWindow
/api/v1/sso/validate1,000 requestsper minute
/api/v1/sso-dash/*100 requestsper minute
/api/v1/sso/authorize30 requestsper minute

Rate-limited responses return 429 Too Many Requests with a Retry-After header.

Caching tip Cache successful /validate responses for short periods (30-60s). Visas don't change unless revoked, so you can safely cache and re-validate periodically.

Prompt File (Action Layer)

The Prompt File is a JSON document that describes your app's API to AI agents. GEOstack reads your Prompt File and auto-generates MCP tools that any AI (ChatGPT, Claude, Gemini) can call.

Think of it as a machine-readable API spec that turns your SaaS into an agent-controllable application — without building any AI yourself.

How it works 1. You define a Prompt File (JSON) in the Developer Console
2. GEOstack validates it and caches it
3. When an AI agent connects via MCP, your tools appear automatically
4. Agent calls a tool → GEOstack proxies the HTTP request to your API

Prompt File Schema

Top-Level Fields

FieldTypeRequiredDescription
versionstringNoSchema version. Default: 1.0
app_namestringYesYour app's display name (1-100 chars)
descriptionstringNoWhat your app does (max 1000 chars)
base_urlstring (URL)YesYour API's base URL (e.g., https://api.yourapp.com)
authobjectNoHow GEOstack authenticates to your API
toolsarrayYes1-100 tool definitions
resourcesarrayNoMCP resource definitions

Auth Object

FieldTypeDefaultDescription
typeenumbearerbearer, header, query, or none
headerstringAuthorizationHeader name for auth
prefixstringBearerToken prefix (for bearer type)
param_namestringQuery param name (for query type)

Tool Definition

FieldTypeRequiredDescription
namestringYesSnake_case name (1-64 chars, e.g., list_contacts)
descriptionstringYesWhat this tool does (shown to AI agents)
scope_requiredstringYesWhich visa scope is needed (e.g., read:contacts)
typeenumNoaction (HTTP proxy) or payment (Stripe). Default: action
methodenumYes*GET, POST, PUT, PATCH, DELETE
pathstringYes*API path (e.g., /api/v1/contacts/{id})
paramsarrayNoQuery parameters (for GET requests)
bodyarrayNoRequest body fields (for POST/PUT/PATCH)
headersobjectNoExtra headers to send with requests
response_mappingobjectNoMap response fields: items, count, message
paymentobjectNoPayment config (only when type: "payment")

* Required for action type tools. Payment tools use defaults.

Parameter / Body Field

FieldTypeRequiredDescription
namestringYesParameter name
typeenumYesstring, number, integer, or boolean
requiredbooleanNoWhether the field is required. Default: false
descriptionstringNoHuman-readable description (shown to AI)
defaultanyNoDefault value if not provided
enumstring[]NoAllowed values (string only)
formatstringNoHint: email, url, iso8601, etc.

Payment Tools

Payment tools route through GEOstack's Payment Layer (Stripe Connect) instead of your HTTP API. This is how the 5% platform fee is collected.

Payment Object

FieldTypeRequiredDescription
amountintegerNoFixed price in cents (e.g., 2900 = $29.00)
currencystringNoISO currency code. Default: usd
modeenumNoone_time or subscription. Default: one_time
recurring_intervalenumNomonth or year (for subscriptions)
price_paramstringNoParameter name for dynamic pricing
product_namestringNoDisplay name on Stripe checkout

Example Payment Tool

{ "name": "subscribe_pro", "description": "Subscribe the user to the Pro plan ($29/month)", "scope_required": "billing:subscribe", "type": "payment", "payment": { "amount": 2900, "currency": "usd", "mode": "subscription", "recurring_interval": "month", "product_name": "FitPlan Pro" } }

MCP Endpoint

GEOstack exposes a Model Context Protocol endpoint that AI agents connect to. Tools are generated dynamically from your Prompt File.

POST /api/v1/mcp

Authentication: Visa token as Bearer token.

Built-in Tools

Every MCP connection includes these meta tools automatically:

ToolDescription
whoamiReturns the agent's visa ID, user, scopes, and expiry
list_scopesLists all scopes granted to this visa

Testing with curl

curl -X POST https://shadow.geostack.xyz/api/v1/mcp \ -H "Authorization: Bearer YOUR_VISA_TOKEN" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
Generate a test visa Go to Developer Console → Prompt File tab → click "Generate Test Visa (1h)" to get a short-lived visa for testing.

Example Prompt Files

CRM Application

{ "version": "1.0", "app_name": "My CRM", "description": "Customer relationship management", "base_url": "https://api.mycrm.com", "tools": [ { "name": "list_contacts", "description": "Search and list contacts", "scope_required": "read:contacts", "method": "GET", "path": "/api/v1/contacts", "params": [ { "name": "search", "type": "string", "required": false, "description": "Search by name" }, { "name": "limit", "type": "integer", "required": false, "default": 20 } ] }, { "name": "create_contact", "description": "Create a new contact", "scope_required": "write:contacts", "method": "POST", "path": "/api/v1/contacts", "body": [ { "name": "name", "type": "string", "required": true }, { "name": "email", "type": "string", "required": true, "format": "email" }, { "name": "phone", "type": "string" } ] } ] }

E-Commerce with Payment

{ "version": "1.0", "app_name": "ShopAI", "description": "AI-powered shopping assistant", "base_url": "https://api.shopai.com", "tools": [ { "name": "search_products", "description": "Search the product catalog", "scope_required": "read:products", "method": "GET", "path": "/api/products", "params": [ { "name": "q", "type": "string", "required": true, "description": "Search query" }, { "name": "category", "type": "string", "enum": ["electronics", "clothing", "home"] } ] }, { "name": "purchase_product", "description": "Purchase a product", "scope_required": "billing:purchase", "type": "payment", "params": [ { "name": "product_id", "type": "string", "required": true } ], "payment": { "price_param": "amount", "currency": "usd", "mode": "one_time" } } ] }

Clinic / Healthcare

{ "version": "1.0", "app_name": "ClinicOS", "description": "Patient management for clinics", "base_url": "https://api.clinicos.com", "tools": [ { "name": "list_patients", "description": "List patients with optional search", "scope_required": "read:patients", "method": "GET", "path": "/api/v1/patients", "params": [ { "name": "search", "type": "string", "description": "Search by name or MRN" } ] }, { "name": "book_appointment", "description": "Book a new appointment for a patient", "scope_required": "write:appointments", "method": "POST", "path": "/api/v1/appointments", "body": [ { "name": "patient_id", "type": "string", "required": true }, { "name": "date", "type": "string", "required": true, "format": "iso8601" }, { "name": "doctor_id", "type": "string", "required": true }, { "name": "reason", "type": "string" } ] }, { "name": "pay_invoice", "description": "Pay an outstanding invoice", "scope_required": "billing:pay", "type": "payment", "params": [ { "name": "invoice_id", "type": "string", "required": true } ], "payment": { "price_param": "amount", "currency": "sar", "mode": "one_time", "product_name": "Medical Invoice" } } ] }