# Gemral Edge REST API v1 — Quickstart

Read-only JSON API over public-record intelligence: congressional stock trades, US federal procurement records, securities-regulator filings, federal regulatory publications, lobbying disclosures, and cross-source convergence signals.

- **OpenAPI 3.1 spec:** https://www.gemral.com/openapi/edge-api-v1.json
- **Base URL:** `https://api.gemral.com/edge-api` (same API also at `https://pgfkbcnzqozzkohwbgbk.supabase.co/functions/v1/edge-api`)
- **Auth:** `Authorization: Bearer gmedge_<40 hex>`
- **Get a key:** https://www.gemral.com/edge/developer (API plan). Keys are shown once and stored only as a hash.
- **Plans and prices:** https://www.gemral.com/edge/pricing
- **Support and service levels:** email support@gemral.com, usually answered within one business day. No 24/7 on-call. Target availability 99.5% per month, without service credits. Live status: https://www.gemral.com/edge/status
- **Data license:** https://www.gemral.com/edge-api-license.md (you may embed the data in your own products with attribution "Data: Gemral Edge"; reselling the raw data as a dataset or feed is not allowed; trials are for evaluation only).
- **Postman / Insomnia / code generators:** import the OpenAPI link above directly (Postman: Import → Link → paste the spec URL). It creates one `edge-api` request with every resource and parameter.
- **Keep the key on your server.** Never put it in code that runs in a browser or a mobile app; the API rejects keys passed in the URL (`?api_key=`).
- **Prefer an AI client?** The same data is available as an MCP server for Claude and ChatGPT (`https://mcp.gemral.com/mcp`); connect it after signing in at https://www.gemral.com/edge/connect

Data is informational only and is not investment advice. Every event keeps a link back to its original public filing.

## 1. First call

```bash
export GEMRAL_KEY="gmedge_..."
curl -s "https://api.gemral.com/edge-api?resource=feed&limit=5" \
  -H "Authorization: Bearer $GEMRAL_KEY"
```

Response shape (list resources):

```json
{ "api_version": "v1", "resource": "feed", "count": 5, "next_cursor": "MjAyNi0xMC0xNF…", "data": [
  { "id": "…", "event_type": "congress_trade", "ticker": "PEP", "headline_key": "edge.event.congressTrade",
    "headline_params": { "side": "buy", "amountMin": 15001, "amountMax": 50000, "politician": "K000389" },
    "resolution_tier": 1, "occurred_at": "2026-09-25T00:00:00+00:00", "created_at": "2026-10-14T13:14:41.798158+00:00",
    "disclosed_at": "2026-10-07T00:00:00+00:00", "disclosed_at_basis": "ptr_filing_date",
    "side": "buy", "amount_min": 15001, "amount_max": 50000, "amount_usd": null,
    "politician_id": "K000389",
    "politician": { "id": "K000389", "full_name": "Ro Khanna", "party": "D", "state": "CA", "chamber": "house" },
    "source_url": null, "source_family": null } ] }
```

`politician_id` resolves with `resource=actor&actor_id=K000389`. Since 20 October 2026 `source_url` and `source_family` (and the other source fields listed in section 8) are always `null`; the fields stay in place so existing parsers keep working. Sources are described by category in section 8 and in the DDQ.

Flat fields save you from parsing `headline_params`: `side`, `amount_min` / `amount_max` (congressional amount ranges) and `amount_usd` (contract, lobbying and insider amounts). `disclosed_at` is the official public filing date when we have it (`disclosed_at_basis`: `ptr_filing_date` for congressional trades, `sec_filing_date` for securities-regulator filings); otherwise it is `null`.

### History and paging (feed)

`since` / `before` take an ISO 8601 date or date-time (UTC) and filter on `created_at`, the time Gemral published the event. `occurred_at` is when the underlying trade or award happened (often weeks earlier because of disclosure lag).

**Point-in-time backtests:** use `disclosed_at` when it is present (the day the filing became public). Otherwise use `created_at`, which is never earlier than the public filing, so it is a safe, conservative bound. Older history was loaded in batches, so many rows share one `created_at` value; that is why `disclosed_at` matters for anything before September 2026.

Page backwards with `cursor`: pass `next_cursor` from the previous response unchanged until it is `null`. Paging is ordered by `created_at`, then `id`, so nothing is skipped when many events share one timestamp.

```bash
# All events published in September 2026, 200 per page
curl -s "https://api.gemral.com/edge-api?resource=feed&since=2026-09-01&before=2026-10-01&limit=200" \
  -H "Authorization: Bearer $GEMRAL_KEY"
# next page: add &cursor=<next_cursor>
```

## 2. Resources

One endpoint, choose the dataset with `resource=`:

| resource | What it returns | Main parameters |
|---|---|---|
| `feed` | Verified events, newest first, with history | `limit` (1–200 json, 1–1000 csv/jsonl), `since`, `before`, `cursor`, `format` |
| `leaderboard` | Members of Congress ranked by estimated return | `window` = `1w`/`1m`/`1y`/`all` |
| `signals` | Top tickers by Alpha Score with levels | `limit` |
| `exposure` | One company's exposure to contracts, trades, policy | `ticker`, `window_days` (1–365) |
| `impact` | Expected impact of one event | `event_id` |
| `beneficiaries` | Public companies behind a contract award | `award_id` or `ticker`, `date` |
| `policy` | Companies affected by a federal regulatory publication | `document_number` or `query` |
| `explain` | Why a ticker scores what it scores | `ticker` |
| `patterns` | Historical stats for one pattern | `pattern_key` |
| `screen` | Multi-source convergence screen | `require` (comma list of event types), `window_days`, `sector`, `limit` |
| `track-record` | Forecast ledger summary + `forecast_scorecard`, the same scorecard as gemral.com/edge/track-record (Beta) | `benchmark` = `sector_etf`/`spy`, `mode` = `pit_replay`/`live` |
| `forecast` | Probabilistic forecast for a ticker (Beta) | `ticker`, `outcome` |
| `intelligence` | Full ticker dossier | `ticker` |
| `actor` | Behavioral profile of a lawmaker, agency or contractor | `actor_id`, `actor_type` |
| `receipts` | Resolved forecast receipts, hit / miss (Beta) | `ticker` (optional), `provenance`, `limit`, `format` |
| `chart-patterns` | Scanner chart patterns on crypto pairs, newest first, with reference levels and a price zone | `symbol`, `timeframe`, `since`, `cursor`, `limit`, `format` |
| `physical` | Physical-infrastructure footprint of one company: data-center build permits, power plant pipeline, regional grid load | `ticker`, `as_of` (optional, YYYY-MM-DD) |

Event types: `gov_contract`, `dod_contract`, `congress_trade`, `tech_shift`, `tech_migration`, `cross_alpha`, `insider_cluster`, `material_contract_8k`, `activist_13d`, `regulatory_shock`, `pre_award`, `options_flag`, `lobbying_filing`, `fed_liquidity_shift`, `fed_bailout`, `dark_pool_surge`, `whale_accumulation`.

### Chart patterns and zones (for charting overlays)

`resource=chart-patterns` returns the patterns the Gemral Scanner detects on crypto pairs only (currently 34 symbols; no stocks), newest first. Each row has the pattern, direction, a 0-100 confidence score, reference levels (`entry_price`, `target_price`, `stop_loss`, `risk_reward_ratio`) and a price zone (`zone_high`, `zone_low`, `zone_type`). `zone_basis` says where the zone comes from: `engine` when the detector outputs the zone, `entry_stop_derived` when it is taken from the entry and invalidation levels. These values describe the detected formation so a charting tool can draw it; they are detections, not trade recommendations or investment advice. Page back with `next_cursor`.

```bash
curl -s "https://api.gemral.com/edge-api?resource=chart-patterns&symbol=BTCUSDT&timeframe=4h&limit=2" \
  -H "Authorization: Bearer $GEMRAL_KEY"
```

```json
{ "api_version": "v1", "resource": "chart-patterns", "count": 2, "next_cursor": "MjAyNi0xMC0xNFQxNjowMDowNy4yNjUyNDUrMDA6MDB8…", "data": [
  { "id": "…", "symbol": "BTC/USDT", "timeframe": "4h", "pattern_name": "Rounding Top", "direction": "BEARISH", "confidence_score": 86,
    "entry_price": 80972.595, "target_price": 70863.777, "stop_loss": 86027.004, "risk_reward_ratio": 2,
    "zone_type": "HFZ", "zone_high": 86027.004, "zone_low": 80972.595, "zone_basis": "entry_stop_derived",
    "detected_at": "2026-10-14T20:00:08.143421+00:00", "provenance": { "source": "crypto_scanner_signals", "engine_version": "v1.2-20260703-evcal" } } ] }
```

The MCP tool `scan_crypto_patterns` returns the same fields (as `levels` and `zone` objects) when the connection uses an API-plan key.

### Physical footprint (physical)

`resource=physical` returns, for one company, the data-center campuses and power plants tied to it, with build-permit activity, power plant pipeline and regional grid load, as publicly known on the `as_of` date (default today; a future date is clamped to today). Locations are as published: `geo_precision` says whether a point is exact or only approximate. Grid load is regional (`granularity: "zone"`), not per site, and satellite and substation fields are `null` until measured. `build_velocity_365d` counts every permit issued in the last 365 days (new and modified) plus planned and under-construction plants; `campus_count_new_365d` counts only campuses whose first permit (version 1, or no version number) was issued in that window. An invalid `ticker` returns 400 `invalid_ticker`; an invalid `as_of` returns 400 `invalid_date`.

```bash
curl -s "https://api.gemral.com/edge-api?resource=physical&ticker=AMZN&as_of=2026-10-15"   -H "Authorization: Bearer $GEMRAL_KEY"
```

```json
{ "api_version": "v1", "resource": "physical", "data": {
  "ticker": "AMZN", "as_of": "2026-10-17", "feed_version": "physical-v1", "access": "full",
  "feed_1": { "congress_trades_90d": 8, "contracts_4q_usd": 196037214 },
  "feed_2": { "masked": false, "site_count": 122, "plant_count": 0, "event_count_365d": 16,
    "sites": [ { "site_name": "Amazon Data Services, Inc. IAD-110, IAD-111, IAD-112, and IAD-113", "kind": "data_center", "state": "VA",
      "lat": 38.7893, "lon": -77.5819, "geo_precision": "point", "eia_subba": null, "latest_permit": "74342-2", "issued_on": "2026-09-29",
      "campus_codes": ["IAD-110", "IAD-111", "IAD-112", "IAD-113"], "nox_tpy_latest": null,
      "substation": { "name": "Atlantic Substation", "kv": 230.0, "distance_km": 0.57, "basis": "map_nearest" },
      "satellite": { "built_index_latest": null, "roof_frac_latest": null, "roof_frac_wide_latest": null, "lst_delta_c_latest": null, "period": null },
      "role": "owner", "related_tickers": [], "stakeholders": [ "…" ] }, "…" ],
    "power_plants": [], "build_velocity_365d": 16, "campus_count_new_365d": 4, "planned_mw_365d": 0, "zone_load": [] } } }
```

The MCP tool `get_physical_footprint` returns the same object.

### Forecast scorecard (track-record)

`resource=track-record` returns `data.forecast_scorecard`, built by the same database function as the public page gemral.com/edge/track-record, so the API and the page show the same numbers. One scorecard covers one outcome definition (`AR(30) > 0` vs the benchmark), one benchmark and one mode; they are never pooled. It reports the base rate, the Brier score, the Brier score of a prior-years point-in-time base rate (`brier_reference`), the Brier skill score (null until enough forecasts resolve), 10 calibration buckets with the mean abnormal return of each, and a per-year table. There is no plain hit rate. A negative skill score means the forecasts did not beat the base rate; the numbers are reported as measured.

```bash
curl -s "https://api.gemral.com/edge-api?resource=track-record&benchmark=spy&mode=live"   -H "Authorization: Bearer $GEMRAL_KEY"
```

### Bulk export: CSV and JSONL

Add `format=csv` or `format=jsonl` to `feed`, `receipts` or `chart-patterns` to pull up to 1,000 rows per call, ready for a data warehouse (Snowflake, BigQuery, Postgres) or a spreadsheet. The CSV has a stable header row; new columns are only ever appended at the end. For `feed`, the next page cursor comes back in the `X-Next-Cursor` response header. Exported rows count toward the monthly export quota of your plan; `X-Export-Rows-Remaining` shows what is left, and a used-up quota returns 429 `export_quota_exceeded`. Under the API plan a key reads the most recent 24 months: without `since`, `feed` starts at that point, every response under a limit carries `X-History-Floor`, and JSON bodies include a `history_limit` note (with the number of older items removed from list results).

```bash
# Nightly load: everything published yesterday as CSV, following the cursor header
curl -s -D headers.txt -o day.csv "https://api.gemral.com/edge-api?resource=feed&format=csv&limit=1000&since=2026-10-13&before=2026-10-14" \
  -H "Authorization: Bearer $GEMRAL_KEY"
grep -i '^x-next-cursor' headers.txt   # present => call again with &cursor=<value>
```

### Rate limits

Each key allows 120 requests per minute by default, and all keys of one account together are capped at 600 per minute (or the highest per-key limit, if you were given a higher one). Every authenticated response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds). Over the limit you get 429 with `Retry-After`.

### Rotating a key

In the developer console, **Rotate** creates a new key with the same label and limit and keeps the old key working for a grace period (24 hours by default), so you can deploy the new key without downtime. Up to 10 keys can be live at once, including keys in their grace period.

## 3. Python

```python
import os, requests

BASE = "https://api.gemral.com/edge-api"
HEADERS = {"Authorization": f"Bearer {os.environ['GEMRAL_KEY']}"}

def edge(resource, **params):
    r = requests.get(BASE, headers=HEADERS, params={"resource": resource, **params}, timeout=30)
    if r.status_code == 429:
        raise RuntimeError("rate limited, retry after %s s" % r.headers.get("Retry-After"))
    r.raise_for_status()
    return r.json()

# Tickers where a congressional trade and a federal contract landed in the same 30 days
hits = edge("screen", require="congress_trade,gov_contract", window_days=30)
print(hits["data"])

# Every event published in September 2026, following the cursor
cursor, rows = None, []
while True:
    page = edge("feed", since="2026-09-01", before="2026-10-01", limit=200, **({"cursor": cursor} if cursor else {}))
    rows += page["data"]
    cursor = page["next_cursor"]
    if not cursor:
        break
print(len(rows), "events")

# Leaderboard for the last year
for row in edge("leaderboard", window="1y")["data"][:10]:
    print(row["rank"], row["name"], row["party"], row["roi"])
```

## 4. Node.js (18+)

```js
const BASE = 'https://api.gemral.com/edge-api';

async function edge(resource, params = {}) {
  const qs = new URLSearchParams({ resource, ...params });
  const res = await fetch(`${BASE}?${qs}`, {
    headers: { Authorization: `Bearer ${process.env.GEMRAL_KEY}` },
  });
  if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error}`);
  return res.json();
}

const { data } = await edge('intelligence', { ticker: 'LMT' });
console.log(data);
```

## 5. Errors

| HTTP | `error` | Meaning |
|---|---|---|
| 400 | `invalid_date` / `invalid_cursor` | Bad `since` / `before` / `cursor` value on `feed` or `chart-patterns` |
| 400 | `invalid_symbol` / `invalid_timeframe` | Bad `symbol` or `timeframe` on `chart-patterns` |
| 400 | `invalid_format` / `format_not_supported_for_resource` | `format` must be json, csv or jsonl; csv/jsonl only for `feed`, `receipts` and `chart-patterns` |
| 400 | `api_key_in_query_not_allowed` | The key was put in the URL; send it only in the `Authorization` header |
| 401 | `missing_or_malformed_api_key` / `invalid_key` | Header missing, wrong format, or key revoked |
| 403 | `api_tier_inactive` | Key is fine but the API plan is not active; the body has `upgrade_url` |
| 403 | `history_limit_exceeded` | `since`, `before`, `cursor` or `date` asks for records older than your plan's history depth (API plan: the most recent 24 months). The body has `earliest_allowed`; older history needs the Internal Use License |
| 404 | `unknown_resource` | Response includes `available` resources |
| 429 | `rate_limit_exceeded` | Per-key or per-account (`scope: user`) minute limit hit; wait `Retry-After` seconds |
| 429 | `export_quota_exceeded` | Monthly csv/jsonl export rows used up (`period_end` in the body) |
| 500 | `auth_error` / `ratelimit_error` / `query_error` | Server-side failure; nothing was served |

## 6. Webhooks (push instead of poll)

Add an HTTPS endpoint in the developer console (https://www.gemral.com/edge/developer) and pick event types (empty = all). A server job checks for new events every 5 minutes and sends each one as a JSON POST. An endpoint only receives events created after it was added. Press **Send test** to get a signed `ping` right away.

```json
{ "event_id": "…", "event_type": "congress_trade", "ticker": "PEP", "headline_key": "edge.event.congressTrade",
  "headline_params": { "side": "buy", "amountMin": 15001, "amountMax": 50000, "politician": "K000389" },
  "resolution_tier": 1, "occurred_at": "2026-09-25T00:00:00+00:00", "created_at": "2026-10-14T13:14:41.798158+00:00",
  "source_url": null, "politician_id": "K000389",
  "api_version": "v1", "delivery_attempt": 1 }
```

Headers: `X-Gemral-Timestamp` (Unix seconds), `X-Gemral-Signature: sha256=<hex>`, `X-Gemral-Event-Id`, `X-Gemral-Delivery-Attempt`, `User-Agent: GemralEdge-Webhook/2.0`.

**Signature:** `sha256=` + hex HMAC-SHA256 of `<X-Gemral-Timestamp> + "." + <raw body>`, keyed with your signing secret. Reject a request when the signature does not match or the timestamp is more than 5 minutes from your clock (this stops replays). Use `X-Gemral-Event-Id` to ignore duplicates.

**Delivery rules:** answer with any 2xx within 5 seconds. Failures are retried up to 6 attempts in total (immediately, then +1 min, +5 min, +30 min, +2 h, +6 h). Redirects are not followed, and the endpoint must resolve to a public address. After 20 failures in a row the endpoint is paused and the account owner gets an email; fix it, press Send test, and switch it back on. Missed events can always be fetched with `resource=feed&since=…`.

Verify in Python:

```python
import hmac, hashlib, time

def verify(raw_body: bytes, timestamp: str, signature: str, secret: str, tolerance: int = 300) -> bool:
    if not timestamp or abs(time.time() - int(timestamp)) > tolerance:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")
```

Verify in Node:

```js
import crypto from 'node:crypto';
const verify = (rawBody, timestamp, signature, secret, tolerance = 300) => {
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > tolerance) return false;
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  return Boolean(signature) && expected.length === signature.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
};
```

Use the raw request body (bytes as received), not a re-serialised JSON object.

### Slack, Discord, Teams, Google Chat, email

- **Slack / Discord:** create an incoming webhook in Slack or Discord, add its URL as an endpoint and choose the format `slack` or `discord` in the console. Gemral sends a short readable message instead of the raw JSON.
- **Microsoft Teams, Google Chat, email, Notion, a CRM:** use Zapier (trigger **Webhooks by Zapier → Catch Hook**) or Make (**Webhooks → Custom webhook**). Paste the hook URL as a `raw` endpoint, press Send test so the tool learns the fields, then add the action you want (post to a Teams channel, send an email, add a row). These tools do not check our signature; keep the hook URL private.

## 7. Spreadsheets

**Excel (Power Query):** Data → Get Data → From Other Sources → Blank Query → Advanced Editor, paste:

```
let
  Key = "gmedge_...",
  Url = "https://api.gemral.com/edge-api?resource=feed&format=csv&limit=1000",
  Source = Csv.Document(Web.Contents(Url, [Headers = [Authorization = "Bearer " & Key]]), [Delimiter = ",", Encoding = 65001, QuoteStyle = QuoteStyle.Csv]),
  Table = Table.PromoteHeaders(Source, [PromoteAllScalars = true])
in
  Table
```

Refresh the query to pull new rows. Store the workbook somewhere only your team can open, because it contains the key.

**Google Sheets (Apps Script):** Extensions → Apps Script, paste, then run `loadEdgeFeed` (add a time-driven trigger to refresh it). Put the key in Project Settings → Script properties as `GEMRAL_KEY`, not in the code.

```js
function loadEdgeFeed() {
  const key = PropertiesService.getScriptProperties().getProperty('GEMRAL_KEY');
  const url = 'https://api.gemral.com/edge-api?resource=feed&format=csv&limit=1000';
  const res = UrlFetchApp.fetch(url, { headers: { Authorization: 'Bearer ' + key }, muteHttpExceptions: true });
  if (res.getResponseCode() !== 200) throw new Error(res.getContentText());
  const rows = Utilities.parseCsv(res.getContentText());
  const sheet = SpreadsheetApp.getActive().getSheetByName('Edge feed') || SpreadsheetApp.getActive().insertSheet('Edge feed');
  sheet.clearContents();
  sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}
```

## 8. Sources

Sources are described by category: U.S. congressional financial disclosure filings (congressional trades); U.S. federal procurement and contract-award public records (contracts); U.S. securities-regulator public filings (insider and material events); U.S. federal regulatory and legislative publications (policy); U.S. federal lobbying disclosure filings (lobbying). The detailed source list is available to a fund's compliance team under NDA (see the DDQ, section 2). Ticker mapping uses `resolution_tier` (1 = exact match, 2 = verified parent company).

**Source fields in responses (change on 20 October 2026).** `source_url`, `doc_url`, `basis_url`, `filing_url`, `pdf_url`, `source_family`, `source_record_id`, `reason_detail` and `sources` are kept with the same names and CSV/JSONL column positions, and are always `null`. This applies to JSON, CSV and JSONL responses and to webhook payloads (Slack and Discord messages no longer include a source link).
