import type { ApiEndpointDoc } from "@dci/core"; import type { Metadata } from "next"; import Link from "next/link"; import { Breadcrumbs } from "@/components/entity/breadcrumbs"; import { ExtLink, KeyValueList } from "@/components/entity/kv"; import { SectionNav } from "@/components/entity/section-nav"; import { SourcesFooter } from "@/components/entity/sources-footer"; import { CodeTabs } from "@/components/intelligence/developers/code-tabs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Page, PageHeader, Section } from "@/components/ui/card"; import { EmptyState, OfflineNotice } from "@/components/ui/states"; import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table"; import { api } from "@/lib/api"; import { fmtInt } from "@/lib/format"; import { routes, SITE_URL } from "@/lib/routes"; import { pageMetadata } from "@/lib/seo"; export const revalidate = 600; export const metadata: Metadata = pageMetadata({ title: "API", description: "DataCenterIndex public REST API: every endpoint with parameters, response types and copy-ready curl / JavaScript / Python examples. Envelope { data, meta, sources }, weak ETags, per-IP rate limits, OpenAPI 3.1.", path: routes.apiDocsPage(), }); const GROUP_LABEL: Record = { facilities: "Facilities", projects: "Projects", operators: "Operators", countries: "Countries", metros: "Markets", "cloud-regions": "Cloud regions", ixps: "Internet exchanges", events: "Events", rankings: "Rankings", dashboard: "Dashboard", stats: "Stats", map: "Map", search: "Search", nearby: "Nearby", explore: "Explore", compare: "Compare", ai: "AI infrastructure", pulse: "Pulse", power: "Power", connectivity: "Connectivity", coverage: "Coverage", "time-machine": "Time machine", sources: "Sources", download: "Download", watchlist: "Watchlist", sitemap: "Sitemap", docs: "Docs", }; const GROUP_ORDER = ["facilities", "projects", "operators", "countries", "metros", "cloud-regions", "ixps", "events", "rankings", "dashboard", "stats", "pulse", "ai", "power", "connectivity", "coverage", "explore", "compare", "search", "map", "nearby", "time-machine", "sources", "download", "watchlist", "sitemap", "docs"]; const linkCls = "text-ink underline decoration-hair-2 hover:text-accent"; function rewrite(s: string): string { return s.replace(/https?:\/\/localhost:8310/g, SITE_URL).replace(/https?:\/\/127\.0\.0\.1:8311/g, SITE_URL); } function MethodBadge({ method }: { method: ApiEndpointDoc["method"] }) { return ( {method} ); } export default async function DevelopersPage() { const r = await api.docsMeta(); const docs = r.ok ? r.data : []; const groups = Array.from(new Set(docs.map((d) => d.group))).sort((a, b) => { const ia = GROUP_ORDER.indexOf(a); const ib = GROUP_ORDER.indexOf(b); return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b); }); const byGroup = new Map(groups.map((g) => [g, docs.filter((d) => d.group === g).sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))])); return ( } /> {!r.ok && }
{SITE_URL}/api/v1 }, { label: "Envelope", value: ( Every response is {"{ data, meta, sources }"} — data is the typed payload, meta carries pagination (total, page, perPage), generatedAt and a methodology sentence when a figure needs one, sources lists the sources (licence, attribution) behind the rows. Downloads stream without an envelope. ), }, { label: "Caching", value: "Weak ETags + s-maxage on every GET — send If-None-Match and expect 304 on unchanged data; lists revalidate within minutes, the change feed within 30 s." }, { label: "Rate limits", value: "Rate-limited per IP; the current window is reported in the x-ratelimit-limit and x-ratelimit-remaining response headers. Bulk use should go through the download endpoints." }, { label: "Errors", value: {'{ "error": "…", "statusCode": 400 | 404 | 429 }'} }, { label: "Partial dates", value: 2027 · 2027-06 · 2027-06-15 · 2027-Q2 · 2027-H1 }, { label: "Honesty", value: "null means not published — never zero. Estimates carry isEstimate; MW figures come with capacityScope / capacitySemantics; coverage fields say how much of an aggregate is backed by a figure." }, ]} />

Types are named after the shared contract (packages/core/src/api-types.ts); the methodology explains how each value is produced. Re-use terms per source on /sources.

{docs.length === 0 ? ( ) : ( <> ({ id: `g-${g}`, label: GROUP_LABEL[g] ?? g, count: byGroup.get(g)?.length ?? 0 }))} className="mt-4" />
{groups.map((g) => (
{(byGroup.get(g) ?? []).map((d) => ( ))}
))}
)}
); } function Endpoint({ d }: { d: ApiEndpointDoc }) { const id = `${d.method}-${d.path.replace(/[^a-z0-9]+/gi, "-")}`; return (

{d.path}

→ {d.responseSchema}

{d.summary}

{d.params.length > 0 ? (
{d.params.map((p) => ( ))}
Parameter In Type Description
{p.name} {p.example && (
e.g. {p.example}
)}
{p.in} {p.type} {p.description || —} {p.example && · e.g. {p.example}}
) : (

No parameters.

)}

Try it: (d.params.find((p) => p.in === "path" && `:${p.name}` === m)?.example ?? m))}`} className="figure text-[11.5px]" />

); }