Geo API, webhooks and MCP

Everything a paid Geo plan gives a program: the records over REST, a CSV export, signed webhooks the minute a record is written, and an MCP server for AI assistants. One key opens all of it. The free pages and the open JSON at /geo/api stay free and need no key.

Machine-readable: OpenAPI 3.1 for both the free JSON and the paid API.

Quick start

  1. Take the API or Team plan.
  2. Open Account, Geo, API keys and create a key. It starts with gk_ and is shown once.
  3. Call the API with the key as a Bearer header:
curl -H "Authorization: Bearer gk_YOUR_KEY" \
  "https://geo.firmtape.com/v1/items?country=IR&min_severity=2&limit=20"

Limits

APITeam
Calls a month (REST and MCP together)20,000100,000
API keys25
Webhook endpoints525
Alert rules25100
CSV export per call7 days31 days
History365 days365 days

The month is the calendar month in UTC. GET /v1/usage is free and says where you stand; the account page shows the same numbers.

Records

GET https://geo.firmtape.com/v1/items

ParameterMeaning
since, untilISO 8601 UTC. Default: the last 7 days. Up to 365 days back.
sectorgov air gps sea internet ground space attention
countryISO 3166-1 alpha-2, for example IR
feedone feed id or several, comma separated; ids are on the sources page and in /v1/sources
min_severity2 notable and above, 3 critical only
qwords in the title
limit1 to 1000, default 200
cursorfrom next in the previous answer

Newest first. When more records match, next holds the full URL of the next page; each page is one call. Times are UTC. Each record:

{
  "uid": "8f1c2a90d4b7e613",
  "feed": "faa_tfr",
  "sector": "air",
  "type": "new",
  "category": "security",
  "severity": 2,
  "routine": false,
  "countries": ["US"],
  "region": "north_america",
  "title": "FAA flight restriction: VIP movement, Washington DC",
  "summary": "",
  "published_utc": "2026-09-17T13:02:11Z",
  "event_utc": "2026-09-18T14:00:00Z",
  "first_seen_utc": "2026-09-17T13:04:02Z",
  "backfill": false,
  "source": "FAA",
  "url": "https://tfr.faa.gov/...",
  "attribution": null,
  "value": null, "baseline": null, "score": null,
  "lat": 38.89, "lon": -77.03,
  "facts": {}
}

routine marks the records the pages hide by default (gunnery exercises, small quakes, green alerts). backfill marks a record found in a source's own history rather than caught live. value, baseline and score are set on spikes: what was measured, its own normal, and the ratio.

GET /v1/sources

Every feed with its source name, when it last answered and how many reads in a row failed.

GET /v1/usage

{ "tier": "api", "month": "2026-09", "calls": 4210, "limit": 20000 }

CSV export

GET /v1/export.csv?since=…&until=…

The same filters as /v1/items, every matching record in one file with every column. A call covers at most 7 days on API and 31 on Team, and counts as one call.

curl -H "Authorization: Bearer gk_YOUR_KEY" -o week.csv \
  "https://geo.firmtape.com/v1/export.csv?since=2026-09-07T00:00:00Z&until=2026-09-14T00:00:00Z"

Webhooks

  1. In Account, Geo, Webhooks add a public https:// address. Copy the signing secret (whsec_…); it is shown once.
  2. In Account, Geo, Alerts add a rule and tick Webhooks. Every record the rule matches is posted to each of your endpoints the minute it is written.
  3. Press Test next to the endpoint to receive a geo.test event.

Each delivery is a POST with a JSON body. Answer with any 2xx within 10 seconds. A delivery is attempted once; its status code and time show under Webhooks in the account, where a failed delivery can be retried. Up to 600 records an hour go to one account's webhooks.

POST /your/path
Content-Type: application/json
X-FirmTape-Webhook-Id: 4k2p9x1m7q3r8s
X-FirmTape-Signature: t=1789650131,v1=5f0c…

{
  "event": "geo.record",
  "sent_utc": "2026-09-17T13:02:12Z",
  "record": { "uid": "8f1c2a90d4b7e613", "feed": "faa_tfr", "sector": "air", "severity": 2, "title": "…", "url": "…", … }
}

Verify the signature

v1 is the hex HMAC-SHA256 of t + "." + raw body with your secret. Compare it in constant time and reject old t values.

// Node.js
const crypto = require("node:crypto");
function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const mac = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
}
# Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, parts["v1"])

MCP for AI assistants

Endpoint https://geo.firmtape.com/mcp (streamable HTTP), with the same key as a Bearer header. Tool calls count toward the month's calls; listing tools does not. Tools:

ToolWhat it returns
geo_recordsrecords newest first; arguments since, until, sector, country, feed, min_severity, q, limit (up to 200), cursor
geo_sourcesevery feed and when it last answered

Claude Code

claude mcp add --transport http firmtape-geo https://geo.firmtape.com/mcp \
  --header "Authorization: Bearer gk_YOUR_KEY"

Cursor, Windsurf and other clients that take headers

{
  "mcpServers": {
    "firmtape-geo": {
      "url": "https://geo.firmtape.com/mcp",
      "headers": { "Authorization": "Bearer gk_YOUR_KEY" }
    }
  }
}

Claude Desktop

{
  "mcpServers": {
    "firmtape-geo": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://geo.firmtape.com/mcp", "--header", "Authorization:${GEO_AUTH}"],
      "env": { "GEO_AUTH": "Bearer gk_YOUR_KEY" }
    }
  }
}

Then ask, for example: "What notable Geo records mention Iran this week? Cite each source." Geo is not on FirmTape's free MCP server; it lives here, behind the key.

Errors

StatusMeaning
400a parameter is wrong; error says which. A malformed request costs no call
401no key, an unknown key or a revoked one
403the key's account has no active API or Team plan, or the key is newer than the plan's key count allows (after a move to a smaller plan the oldest keys keep working)
429the month's calls are used; Retry-After says when the count resets

Credit

Every record carries its source and url, and attribution where the source asks for a specific citation (UCDP, GDACS, Copernicus, RIPE NCC and others). Show them next to anything you display. Team may show records to its own customers with that credit; no plan allows reselling the records as a feed. Records are public information with their time and source: context for a session, never a trading signal.