SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
9.1 KB · 148 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { Container, Note, PageHeader, Section } from '@/components/ui/section';4import { CONTACT_EMAIL, PUBLIC_API_BASE, routes } from '@/lib/site';56export const metadata: Metadata = { title: 'API & data', description: 'Public JSON API for Company Atlas: companies, events, timelines, metrics, rankings, industries, countries, search, SSE streaming and exports.' };78const GROUPS: { title: string; rows: [string, string, string][] }[] = [9  {10    title: 'Platform',11    rows: [12      ['GET', '/stats', 'Global counters: companies, sensors, observations, changes, events, dataset age (cached 60 s)'],13      ['GET', '/stats/history?days=90', 'Daily global aggregates and the activity index'],14      ['GET', '/system', 'Public aggregate health'],15      ['GET', '/pulse', 'Homepage aggregate (live, movers, hiring, launches, pricing, AI, industries, countries, trending, index, map)'],16      ['GET', '/live?limit=50&since=&event_type=&min_importance=', 'Latest active events (no cache)'],17      ['GET', '/live/stream', 'Server-sent events: `event: event` with an Event JSON, `event: heartbeat` every 20 s; `?since=` supported'],18    ],19  },20  {21    title: 'Companies',22    rows: [23      ['GET', '/companies?q=&country=&industry=&tier=&public=&sort=&sparkline=1', 'Paginated CompanyCard rows (`page`, `per_page` ≤ 200)'],24      ['GET', '/companies/{slug}', 'Detail: aliases, domains, relationships, metrics with confidence, coverage, signals, sparklines'],25      ['GET', '/companies/{slug}/events', 'Events with filters `event_type, event_subtype, since, until, min_importance, surface`'],26      ['GET', '/companies/{slug}/timeline?filter=all|products|jobs|pricing|leadership|locations|legal|news|developer', 'Grouped by day'],27      ['GET', '/companies/{slug}/metrics?days=90', 'Current metrics + series'],28      ['GET', '/companies/{slug}/jobs?status=open|removed|all&ai=1', 'Listings + summary (`meta.summary`)'],29      ['GET', '/companies/{slug}/people · /products · /pricing · /locations · /news', 'Reconciled entities with first/last seen'],30      ['GET', '/companies/{slug}/sensors · /history · /similar', 'Sensors, historical page viewer index, similar companies'],31      ['GET', '/companies/compare?companies=stripe,adyen,block', 'Side-by-side metrics, series, events, jobs, locations (2–6)'],32    ],33  },34  {35    title: 'Provenance',36    rows: [37      ['GET', '/sensors/{id} · /snapshots · /changes', 'Sensor with company and latest snapshot; its versions and changes'],38      ['GET', '/snapshots/{id}', 'Normalised text, semantic blocks, extracted fields (≤ 200 kB)'],39      ['GET', '/snapshots/{id}/diff/{other_id}', 'Block-level diff computed on demand'],40      ['GET', '/changes/{id}', 'Change with diff, structured delta and derived events'],41      ['GET', '/events/{id}', 'Event with every corroborating source and detection time'],42    ],43  },44  {45    title: 'Events, rankings, atlases',46    rows: [47      ['GET', '/events?event_type=&country=&industry=&since=&min_importance=&min_confidence=&q=&origin=&sort=', 'Paginated events'],48      ['GET', '/events/types · /events/summary?days=7&group=type|industry|country', 'Taxonomy counts and deltas'],49      ['GET', '/rankings?kind=most_active|hiring_growth|…&window=24h|7d|30d|90d|1y&country=&industry=', 'Ranked CompanyCards with value and delta'],50      ['GET', '/industries · /industries/{slug} · /countries · /countries/{code}', 'Living indices per industry and country'],51      ['GET', '/signals?scope=company|industry|country|global · /trends?window=7d · /map?metric=events_30d · /index', 'Signals, trending terms, map buckets, Global Corporate Activity Index'],52    ],53  },54  {55    title: 'Search, watchlists, exports',56    rows: [57      ['GET', '/search?q=&types=companies,events,… · /search/suggest?q= · /ask?q=', 'Grouped search, fast suggestions, natural-language routing'],58      ['GET/POST/DELETE', '/watchlist · /watchlist/{slug} · /alerts · /alerts/{id} · /alerts/deliveries', 'Owner-token endpoints (`X-CA-Owner-Token`, ≥ 24 chars, stored hashed)'],59      ['GET', '/export/events.{json,ndjson,csv}?since=&event_type=&country=&limit=10000', 'Streamed export'],60      ['GET', '/export/companies.{json,ndjson,csv}?country=&industry= · /export/jobs.ndjson?company=&since=', 'Streamed exports'],61      ['GET', '/sitemap?kind=companies|industries|countries&page= · /methodology', 'Indexable slugs; metric definitions'],62    ],63  },64];6566export default function ApiPage() {67  const base = PUBLIC_API_BASE;68  return (69    <Container>70      <PageHeader eyebrow="Developers" title="API & data" lede="JSON over HTTPS, UTC ISO-8601 timestamps, immutable ids (co_…, sen_…, evt_…) and slugs for public URLs. Public GET endpoints need no key. Errors are `{ detail }` with 4xx/5xx." />71      <div className="prose-atlas max-w-3xl text-sm text-ink-2">72        <p>73          Base URL: <code>{base}</code>. Pagination: <code>?page=1&per_page=25</code> (max 200) → <code>{'{ items, page, per_page, total, pages }'}</code>. Unknown parameters are ignored. Interactive OpenAPI docs: <a href={`${base}/docs`}>{base.replace(/^https?:\/\//, '')}/docs</a>.74        </p>75        <h2>Quick start</h2>76        <pre>77          <code>{`# latest structured events78curl -s "${base}/live?limit=5" | jq '.items[] | {company: .company.display_name, type: .event_type, title, confidence_label, source_url}'7980# a company with its metrics, coverage and signals81curl -s "${base}/companies/stripe" | jq '{display_name, metrics, counts, coverage}'8283# pricing events in Canada since September, most important first84curl -s "${base}/events?event_type=PRICING&country=CA&since=2026-09-01T00:00:00Z&sort=importance"8586# rankings: fastest hiring growth over 30 days in fintech87curl -s "${base}/rankings?kind=hiring_growth&window=30d&industry=fintech"8889# stream (SSE)90curl -N "${base}/live/stream"9192# export as NDJSON93curl -s "${base}/export/events.ndjson?event_type=LEADERSHIP&limit=1000" > leadership.ndjson`}</code>94        </pre>95        <h2>Watchlists without an account</h2>96        <pre>97          <code>{`TOKEN=$(uuidgen | tr -d -)$(uuidgen | tr -d -)98curl -s -X POST "${base}/watchlist" -H "X-CA-Owner-Token: $TOKEN" -H "content-type: application/json" -d '{"company":"stripe"}'99curl -s "${base}/watchlist" -H "X-CA-Owner-Token: $TOKEN"100curl -s -X POST "${base}/alerts" -H "X-CA-Owner-Token: $TOKEN" -H "content-type: application/json" \\101  -d '{"name":"Stripe pricing","company":"stripe","condition":{"event_types":["PRICING"],"min_importance":0.5},"channel":"webhook","target":"https://example.com/hook"}'`}</code>102        </pre>103      </div>104      {GROUPS.map((g) => (105        <Section key={g.title} eyebrow="Endpoints" title={g.title}>106          <div className="table-scroll">107            <table className="data-table compact">108              <thead>109                <tr>110                  <th>Method</th>111                  <th>Path</th>112                  <th>Notes</th>113                </tr>114              </thead>115              <tbody>116                {g.rows.map(([m, p, n]) => (117                  <tr key={p}>118                    <td className="mono text-xs text-ink-3">{m}</td>119                    <td className="mono wrap text-xs text-ink">{p}</td>120                    <td className="wrap text-ink-2">{n}</td>121                  </tr>122                ))}123              </tbody>124            </table>125          </div>126        </Section>127      ))}128      <Section eyebrow="Conventions" title="Rate limits, caching, licensing">129        <div className="prose-atlas max-w-3xl text-sm text-ink-2">130          <ul>131            <li>Anonymous: 120 requests / minute per IP; bursts above that return 429 with a Retry-After header. Higher limits, webhooks and bulk datasets on request by email to <a href={`mailto:${CONTACT_EMAIL}`}>{CONTACT_EMAIL}</a> (free for research).</li>132            <li>133              Public aggregates (<code>/pulse</code>, <code>/stats</code>, <code>/rankings</code>, <code>/industries</code>, <code>/countries</code>) are cached 60 s and served with <code>Cache-Control: public, max-age=60</code>; <code>/live*</code>, owner and admin routes are <code>no-store</code>.134            </li>135            <li>Metrics are 0–100 floats rounded to one decimal, except hiring momentum (percentage, may be negative) and open jobs (integer). A metric without inputs is omitted or null — never zero.</li>136            <li>Company lookups accept slug or id; slugs may change, ids never do. Unknown → 404 <code>{'{ "detail": "company not found" }'}</code>.</li>137            <li>Redistribution: derived data (events, metrics, entity facts, metadata) may be reused with attribution to Company Atlas and a link to the event page; raw page content is not redistributed.</li>138            <li>139              Careful language is part of the contract: <code>status: no_longer_listed</code>, <code>confidence_label</code> and <code>origin</code> (deterministic · llm · hybrid · backfill) ship with every event — see <Link href={routes.methodology()}>methodology</Link>.140            </li>141          </ul>142        </div>143        <Note className="mt-4">Shapes are documented in the repository (`docs/API.md`) and mirrored by the web client’s TypeScript types.</Note>144      </Section>145    </Container>146  );147}148