spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { ENDPOINT_GROUPS, RATE_LIMITS } from '@/components/meta/endpoints';4import { ATTRIBUTION, DISCLAIMER } from '@/components/meta/legal';5import { Callout, Code, DocLayout, DocSection, Prose } from '@/components/meta/prose';6import { Container, PageHeader } from '@/components/ui/section';7import { CONTACT_EMAIL, SITE_URL, routes } from '@/lib/site';89export const metadata: Metadata = {10 title: 'API for developers — free JSON access to the orbital catalog',11 description: 'The SatelliteIndex REST API: satellites, live SGP4 positions, constellations, operators, countries, launches, debris, reentries, events and statistics as JSON. Free during the MVP.',12 alternates: { canonical: `${SITE_URL}/developers` },13 openGraph: { title: 'API | SatelliteIndex', description: 'Free JSON API for everything in Earth orbit — the same API that powers this site.', url: `${SITE_URL}/developers` },14 twitter: { card: 'summary', title: 'API | SatelliteIndex', description: 'Free JSON API for everything in Earth orbit.' },15};1617const BASE = `${SITE_URL}/api/v1`;1819const TOC = [20 { id: 'basics', label: 'Basics' },21 { id: 'envelope', label: 'Response envelope' },22 ...ENDPOINT_GROUPS.map((g) => ({ id: `ep-${g.title.toLowerCase().replace(/[^a-z]+/g, '-')}`, label: g.title })),23 { id: 'positions-format', label: 'Batch positions format' },24 { id: 'rate-limits', label: 'Rate limits' },25 { id: 'attribution', label: 'Attribution & terms' },26 { id: 'roadmap', label: 'Keys and tiers' },27];2829export default function DevelopersPage() {30 return (31 <Container>32 <PageHeader eyebrow="Developers" title="The SatelliteIndex API" lede="Every page on this site is rendered from the public JSON API documented here — there is no private dataset behind it. Free, no key required during the MVP, rate-limited per client.">33 <p className="mt-4 flex flex-wrap gap-x-4 gap-y-1 text-sm">34 <a href="/api/v1/docs" className="link">35 Interactive docs (Swagger)36 </a>37 <a href="/api/v1/openapi.json" className="link">38 OpenAPI 3 schema39 </a>40 <Link href={routes.status()} className="link">41 Status42 </Link>43 </p>44 </PageHeader>4546 <DocLayout toc={TOC}>47 <div>48 <DocSection id="basics" title="Basics">49 <Prose>50 <p>51 Base URL: <code>{BASE}</code>. All endpoints are <code>GET</code>, return <code>application/json</code> encoded in UTF-8, and use UTC ISO-8601 timestamps. Units are kilometres, km/s, kilograms, degrees and minutes. Countries are ISO 3166-1 alpha-2 codes. Numbers aggregated by the database may arrive as strings — parse them as decimals.52 </p>53 <p>54 Satellites can be addressed by slug (<code>iss-zarya-25544</code>), NORAD catalog number (<code>25544</code>) or COSPAR designator (<code>1998-067A</code>). Internal ids are prefixed ULIDs (<code>sat_…</code>) and are stable; NORAD and COSPAR are source identifiers and are kept as such.55 </p>56 </Prose>57 <Code label="curl">{`curl -s "${BASE}/satellites/25544" | jq '.data | {name, norad_id, status, orbit_class, live}'`}</Code>58 </DocSection>5960 <DocSection id="envelope" title="Response envelope and pagination">61 <Prose>62 <p>63 Single resources return <code>{'{ data, meta }'}</code>; list endpoints add a <code>pagination</code> block. <code>meta.request_id</code> is the id you will find in our server logs if you report a problem; <code>meta.generated_at</code> is the server time of the response (responses may be cached for up to a few minutes).64 </p>65 </Prose>66 <Code label="Shape">{`{67 "data": [ … ],68 "pagination": { "page": 1, "page_size": 50, "total": 17026, "pages": 341 },69 "meta": { "request_id": "b382117229134a57", "generated_at": "2026-09-11T18:17:53Z" }70}`}</Code>71 <Prose className="mt-3">72 <p>73 Errors use the same envelope with an <code>error</code> object: <code>{'{ "error": { "title", "detail", "status" } }'}</code> and the matching HTTP status (404 unknown entity, 422 invalid parameter, 429 rate limited, 503 upstream/database unavailable). Paginate with <code>page</code> and <code>page_size</code> (1–200; sitemap feeds allow more).74 </p>75 </Prose>76 </DocSection>7778 {ENDPOINT_GROUPS.map((g) => (79 <DocSection key={g.title} id={`ep-${g.title.toLowerCase().replace(/[^a-z]+/g, '-')}`} title={g.title} eyebrow="Endpoints">80 <ul className="divide-y divide-rule border-y border-rule">81 {g.endpoints.map((e) => (82 <li key={e.path} className="py-3">83 <div className="flex flex-wrap items-center gap-2">84 <span className="mono rounded bg-plane-3 px-1.5 py-0.5 text-[11px] text-accent">{e.method}</span>85 <code className="mono break-all text-sm text-ink">{e.path}</code>86 {e.bucket && <span className="text-[11px] text-ink-3">rate bucket: {e.bucket}</span>}87 </div>88 <p className="mt-1 text-sm text-ink-2">{e.summary}</p>89 <Code className="mt-2">{`curl -s "${BASE}${e.example}"`}</Code>90 </li>91 ))}92 </ul>93 </DocSection>94 ))}9596 <DocSection id="positions-format" title="Batch positions format">97 <Prose>98 <p>99 <code>/orbit/positions</code> returns the position of every object in the propagator (tens of thousands) at two instants, <code>t0</code> and <code>t1 = t0 + step_s</code>, so a client can interpolate smoothly between polls. To keep the payload small the response is a set of <strong>parallel arrays</strong> rather than one object per satellite: index <code>i</code> of each array describes the same object.100 </p>101 </Prose>102 <Code label="Fields">{`{103 "t0": "…", "t1": "…", "step_s": 60, "count": N, "total_tracked": N,104 "fields": ["norad", "cls", "mission", "active", "pos", "vel"],105 "norad": [25544, …], // NORAD id per object106 "cls": [0, …], // index into legend.cls (orbit class)107 "mission": [8, …], // index into legend.mission108 "active": [1, …], // 1 = ACTIVE status109 "pos": [lat0, lon0, alt0, lat1, lon1, alt1, …], // 6 numbers per object (deg, deg, km)110 "vel": [v0, v1, …], // km/s at t0 and t1111 "legend": { "cls": ["LEO","MEO","GEO","HEO","OTHER"], "mission": [ … ] }112}`}</Code>113 <Prose className="mt-3">114 <p>Positions are derived from the latest element set of each object and inherit its age; check <code>/sources/status</code> for the median element age before relying on them.</p>115 </Prose>116 </DocSection>117118 <DocSection id="rate-limits" title="Rate limits">119 <Prose>120 <p>Limits are applied per client IP over a sliding 60-second window. Exceeding a bucket returns <code>429 Too Many Requests</code> with the bucket name in the error detail. Cache responses on your side; most datasets change hourly at most.</p>121 </Prose>122 <div className="mt-4 overflow-x-auto">123 <table className="data-table stack sm:min-w-[480px]">124 <thead>125 <tr>126 <th>Bucket</th>127 <th className="num">Requests / min</th>128 <th>Applies to</th>129 </tr>130 </thead>131 <tbody>132 {RATE_LIMITS.map((r) => (133 <tr key={r.bucket}>134 <td className="primary mono text-sm">{r.bucket}</td>135 <td data-label="Requests / min" className="num tnum text-sm">136 {r.perMinute}137 </td>138 <td data-label="Applies to" className="mono text-xs text-ink-2">139 {r.applies}140 </td>141 </tr>142 ))}143 </tbody>144 </table>145 </div>146 </DocSection>147148 <DocSection id="attribution" title="Attribution and terms">149 <Prose>150 <p>151 If you republish data from this API you must keep the upstream attributions. The orbital elements and catalog come from CelesTrak, whose terms require credit: <strong>“Orbital data courtesy of CelesTrak.”</strong> Derived values (orbit class, mission type, constellation membership, activity score, density) should be credited to SatelliteIndex with a link to the <Link href={routes.methodology()} className="link">methodology</Link>. The full source list with licenses is on <Link href={routes.sources()} className="link">/sources</Link>.152 </p>153 <p>{ATTRIBUTION}</p>154 </Prose>155 <Callout tone="warn">{DISCLAIMER}</Callout>156 <p className="mt-3 text-sm text-ink-3">157 Full <Link href={routes.terms()} className="link">terms of use</Link>.158 </p>159 </DocSection>160161 <DocSection id="roadmap" title="Keys and tiers">162 <Prose>163 <p>164 The API is <strong>free during the MVP</strong> and requires no key. API keys, higher rate limits and paid tiers for heavy or commercial use will come later; the public endpoints documented here will keep a free tier. If you are building on the API, say hello at <a href={`mailto:${CONTACT_EMAIL}`} className="link">{CONTACT_EMAIL}</a> so we can warn you before any breaking change.165 </p>166 </Prose>167 </DocSection>168 </div>169 </DocLayout>170 </Container>171 );172}173