SokkoSokko
← Back to blog

Hacker News API Complete Reference Guide for Developers

Sokko21 min read

You're building a live HN dashboard, a newsletter generator, or an AI agent that needs to answer questions about developer discussions. The first request looks simple, but the architecture becomes less obvious once you discover that Hacker News exposes two different API surfaces: the official Firebase-backed API for live, canonical objects, and the Algolia-powered search API for full-text discovery and filtering.

Use the official API when you need current feeds, item relationships, comments, or user profiles. Use Algolia when you need queries such as stories about Rust, author history, date filters, point thresholds, or comment analytics. A reliable production integration often calls both, using Algolia to discover relevant IDs and Firebase to hydrate canonical item data.

Table of Contents

Hacker News API Overview and What This Guide Covers

A production HN dashboard often needs two data paths. The official Firebase API supplies live, canonical objects, while Algolia provides indexed search and filtering. Keeping those roles separate prevents a common mistake: treating search results as a complete replacement for the source feed.

The official API launched on October 7, 2014, after Y Combinator announced a public, read-only Firebase API. Its versioned base path is https://hacker-news.firebaseio.com/v0/. The official Hacker News API repository documents its JSON object model and endpoint structure. Responses represent stories, comments, jobs, polls, poll options, and users directly, with relationships stored as integer IDs. A thread reader therefore assembles its view through multiple item requests, and must handle deleted items or missing fields.

HN Search is a separate REST service built on Algolia. Its documented base path is https://hn.algolia.com/api, and the official HN Search API documentation describes text search, date-ordered search, item and user lookup, filters, and pagination.

A comparison infographic between the Firebase read-only API and the Algolia search API for Hacker News data.

Which API should you call

Choose Firebase for:

  • Live feeds: Top, new, best, Ask HN, Show HN, and job story lists.

  • Canonical objects: Complete item payloads, comment trees, and user profiles.

  • Relationships: kids, parent, descendants, and other HN-native fields.

Choose Algolia for:

  • Free-text queries: Search titles, story text, and comments.

  • Structured discovery: Combine queries with tags and numeric filters.

  • Analytics workflows: Explore authors, dates, scores, and indexed results.

A practical integration frequently uses both. Query Algolia to find relevant IDs, then fetch canonical objects from Firebase before rendering or passing data to an agent. Both surfaces are read-only for application developers, so neither accepts stories, comments, or votes. Use Firebase's /v0/ prefix for source data and Algolia for indexed discovery.

Official HN API Endpoints Reference

The official Firebase API uses a small set of read-only endpoints, each with a specific role. Verify path names and object definitions against the maintained API reference on GitHub before shipping a client.

MethodPathReturn typePurpose
GET/v0/topstories.jsonArray of integer IDsCurrent top stories
GET/v0/newstories.jsonArray of integer IDsRecently submitted stories
GET/v0/beststories.jsonArray of integer IDsStories currently considered best
GET/v0/askstories.jsonArray of integer IDsAsk HN submissions
GET/v0/showstories.jsonArray of integer IDsShow HN submissions
GET/v0/jobstories.jsonArray of integer IDsJob postings
GET/v0/item/{id}.jsonJSON object or nullStory, comment, job, poll, or poll option
GET/v0/user/{username}.jsonJSON object or nullUser profile
GET/v0/updates.jsonJSON objectRecently changed items and users
GET/v0/maxitem.jsonIntegerHighest item ID currently known

Feed endpoints return flat arrays of IDs, not hydrated stories. Fetch the list, choose the IDs required by the page, and request each object through /v0/item/{id}.json. A bounded worker pool is safer than an unbounded Promise.all, especially when a feed contains many entries or several IDs no longer resolve.

maxitem.json supports archive walks and checks for newly allocated IDs. updates.json can guide cache invalidation, but it should not be treated as the only freshness signal. Refresh the item being displayed and handle a null response, since an item may be deleted or unavailable by the time the follow-up request runs.

All ordinary reads use GET requests and require no authentication header. Successful responses are JSON, but clients still need to inspect the HTTP status before parsing. A malformed path, transient Firebase failure, or unavailable resource can return an error response rather than the object shape your renderer expects. Keep endpoint construction centralized so Firebase paths and Algolia search requests cannot be mixed accidentally.

Item and User JSON Response Shapes

An HN item is intentionally sparse, and its shape depends on the item type. A story may provide a title and URL, while an Ask HN post usually carries its content in text. Comments include parent and text; jobs can include both url and text without matching a regular story's fields. Treat the response as input to a normalizer, not as a fixed record.

Fields to preserve in a normalized item

FieldTypeAvailability and use
idIntegerStable HN item identifier
typeStringUsually story, comment, job, poll, or pollopt
byString or absentAuthor username
timeInteger or absentUnix timestamp in seconds
titleString or absentStory, job, or poll title
urlString, null, or absentExternal destination, often absent for Ask HN
scoreInteger or absentStory or poll score
descendantsInteger or absentTotal comment count for a story
kidsArray of integers or absentChild comment IDs
textHTML string, null, or absentSelf-text or comment body
deadBoolean or absentModeration state
deletedBoolean or absentDeletion state
parentInteger or absentParent story or comment for a comment

A feed renderer generally needs id, title, url, text, by, time, score, and descendants. Thread views also require kids and parent, plus the moderation flags. Code should tolerate absent fields on every response, even when an earlier item included them.

Ask HN posts often omit url because their content is in text. Comment text follows the same HTML convention. Sanitize both values before rendering them in a page. A missing URL is a valid self-contained post, not a failed request.

User objects are sparse too

A user object can contain id, created, karma, about, and submitted. The created value is a Unix timestamp, karma is an integer, and submitted contains item IDs. Keep those IDs without hydrating the entire history during a profile request. Fetch only the slice required by the current view.

Deleted items commonly resolve as tombstones. They may include deleted: true, type: "deleted", and many null or absent fields. Dead items can retain identifying metadata while hiding their text. A purged or unavailable ID can instead return JSON null, so the normalizer needs an explicit missing state.

export interface NormalizedHnItem {
  id: number;
  type: "story" | "comment" | "job" | "poll" | "pollopt" | "deleted";
  author: string | null;
  title: string | null;
  url: string | null;
  textHtml: string | null;
  timeUnix: number | null;
  score: number | null;
  descendants: number | null;
  childIds: number[];
  parentId: number | null;
  dead: boolean;
  deleted: boolean;
}

Use the Firebase shape as the source of record for item hydration. For search and analytics, Algolia results use a different document shape and should be mapped into this interface before shared rendering or agent code consumes them. That boundary prevents missing Firebase fields and search-only fields from leaking into application logic.

Calling the HN API with curl, JavaScript, and Python

A production feed usually makes three Firebase requests: fetch a feed's item IDs, hydrate one item, and load a user profile. The documented /v0/ base path serves all three. Use curl -i while diagnosing proxy, TLS, or upstream failures because it exposes the status and response headers.

Curl

curl -i https://hacker-news.firebaseio.com/v0/topstories.json
curl -i https://hacker-news.firebaseio.com/v0/item/8863.json
curl -i https://hacker-news.firebaseio.com/v0/user/pg.json

These reads need no special request header. The feed endpoint returns an integer array. Item and user requests return a JSON object or null. Firebase can also omit fields, so deserialize defensively rather than assuming every item has a title, URL, text, or author.

Node.js fetch with cancellation

const BASE = "https://hacker-news.firebaseio.com/v0";

async function getJson(path, timeoutMs = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(`${BASE}${path}`, {
      signal: controller.signal,
    });

    if (!response.ok) {
      throw new Error(`HN returned ${response.status} for ${path}`);
    }

    return await response.json();
  } finally {
    clearTimeout(timer);
  }
}

const ids = await getJson("/topstories.json");
const firstItem = await getJson(`/item/${ids[0]}.json`);
const user = await getJson("/user/pg.json");

console.log({ firstItem, user });

The timeout prevents a stalled Firebase connection from holding a feed request indefinitely. Cache the ID list separately from hydrated items, then refresh each according to its freshness needs.

Python requests with a reusable session

import requests

BASE = "https://hacker-news.firebaseio.com/v0"
session = requests.Session()

def get_json(path: str):
    response = session.get(f"{BASE}{path}", timeout=5)
    response.raise_for_status()
    return response.json()

ids = get_json("/topstories.json")
item = get_json(f"/item/{ids[0]}.json")
user = get_json("/user/pg.json")

print(item)
print(user)

A feed ID may resolve to a live, dead, deleted, or missing item. Filter None after hydration and preserve the distinction between an unavailable record and a request failure. The same normalized item model can later accept Firebase records and Algolia search hits without coupling rendering or agent code to either response shape.

HN Search API on Algolia for Discovery and Analytics

Firebase exposes ordered IDs and item relationships. Algolia adds indexed search, filters, and analytics-friendly result metadata. Use /search for relevance-oriented queries, /search_by_date for recent discovery, /items/:id for an indexed item, and /users/:username for indexed author activity.

A query can combine text with HN Search syntax:

curl "https://hn.algolia.com/api/v1/search?query=rust%20lang%3Arust%20points%3A%3E100&tags=story"

URL-encode parameters instead of assembling raw query strings. For date-bounded searches, choose search_by_date and use numeric filters such as created_at_i>.... tags separates stories from comments, while numericFilters can constrain points, timestamps, and other indexed numeric fields.

import requests

params = {
    "query": "async runtime",
    "tags": "story",
    "numericFilters": "points>100",
    "page": 0,
    "hitsPerPage": 20,
}

response = requests.get(
    "https://hn.algolia.com/api/v1/search",
    params=params,
    timeout=5,
)
response.raise_for_status()

payload = response.json()
for hit in payload.get("hits", []):
    print(hit.get("objectID"), hit.get("title"))

The response envelope includes hits, nbHits, page, nbPages, hitsPerPage, and processingTimeMS. Dashboards can display returned records alongside counts and pagination state without deriving those values locally. Algolia hits can have missing fields, so read optional properties defensively. /users/:username supports indexed author activity, while /items/:id retrieves an indexed object.

An infographic showing four core endpoints of the Hacker News search API powered by Algolia.

Use Algolia to discover candidate IDs, then fetch selected records from Firebase when the canonical HN shape matters. That split also suits a knowledge base architecture: search identifies candidate documents, and the canonical fetch supplies content for storage or later AI processing.

Pagination Patterns Across Firebase and HN Search

Firebase and Algolia paginate different resources. Feed endpoints return an ordered ID array, so your application chooses the slice and controls item hydration. Algolia returns search pages with explicit metadata, which makes page walking predictable.

APIEndpoint(s)Pagination parameterDefaultRecommended pattern
FirebaseFeed endpointsNone on feed callsFull returned ID arraySlice IDs, then hydrate with bounded concurrency
Firebasemaxitem.jsonItem ID cursorCurrent maximum IDWalk backward and stop at your retention boundary
Algoliasearchpage, hitsPerPageAPI-defined response defaultsStop at nbPages or an empty hits array
Algoliasearch_by_datepage, hitsPerPageAPI-defined response defaultsPreserve filters while incrementing page

A Firebase worker should cap concurrent item requests and preserve input order:

async function mapPool(values, worker, concurrency = 10) {
  const output = new Array(values.length);
  let next = 0;

  async function run() {
    while (true) {
      const index = next++;
      if (index >= values.length) return;
      output[index] = await worker(values[index]);
    }
  }

  await Promise.all(
    Array.from(
      { length: Math.min(concurrency, values.length) },
      () => run()
    )
  );

  return output;
}

const ids = await getJson("/topstories.json");
const items = await mapPool(ids.slice(0, 30), id => getJson(`/item/${id}.json`));

For Algolia, retain the query and filters while increasing page. Check both hits and nbPages, because an empty page is a useful defensive stop condition.

async function* searchPages(query, filters) {
  let page = 0;

  while (true) {
    const params = new URLSearchParams({
      query,
      page: String(page),
      hitsPerPage: "20",
      ...filters,
    });

    const result = await fetch(
      `https://hn.algolia.com/api/v1/search?${params}`
    ).then(response => {
      if (!response.ok) throw new Error(`Search failed: ${response.status}`);
      return response.json();
    });

    if (!result.hits?.length) return;

    yield* result.hits;
    page += 1;

    if (page >= result.nbPages) return;
  }
}

Use Algolia for discovery and Firebase for canonical item hydration when fields, relationships, or current deletion state matter. maxitem is an archive cursor, not a replacement for HN's feed ordering.

Rate Limits, Caching, and Performance Tactics

The phrase “no rate limit” does not mean unlimited bursts are safe. The initial Firebase API release was described that way at launch, yet applications still compete for network connections and upstream capacity. Treat the official API documentation as the operational reference, then add caching, bounded concurrency, timeouts, and backoff in your client.

Do not build production capacity around an unverified connection ceiling. The same caution applies to Algolia plan limits and response-size constraints. Service behavior and quotas can change, so check current limits before turning them into hard-coded assumptions.

A cache that works

Separate feed ID lists from item bodies. Feed membership changes on a different schedule from an individual story, while user profiles often need a third freshness policy.

  • Feed IDs: Refresh according to the product's needs and serve stale data briefly if the upstream request fails.

  • Item bodies: Key entries by numeric item ID. Replace them when a refresh observes changed content or relationships.

  • Search results: Include the normalized query, filters, page, and sort mode in the cache key.

  • Failures: Cache negative results briefly, but let null expire so a transient omission does not become permanent.

HTTP validators can reduce repeated transfers when supported, but Firebase responses should not be assumed to provide ETags with relational-resource semantics. An application-level cache gives you consistent behavior across Firebase and Algolia.

A small cache usually saves more work than faster JSON parsing. Keep request timeouts separate from retry policy, and add jitter to scheduled refreshes so multiple workers do not refresh together. A Node process can use an LRU library. Multiple instances need shared state, such as Redis, if they must coordinate freshness and request suppression.

For AI workflows, store the raw item before generating a summary. You can then regenerate derived text without fetching the same thread repeatedly. That raw layer also helps an agent distinguish current source data from an older generated response.

A short-lived API cache and persistent memory for AI agents serve different purposes. The cache expires to protect freshness. Persistent memory retains selected facts, decisions, or summaries for later tasks, so it needs its own update and deletion rules.

A list of five essential rate limits, caching, and performance tactics for optimizing API usage and managing data requests.

Error Handling for Dead, Deleted, and Missing Items

A production HN client must treat feed IDs as references, not guaranteed stories. Firebase may return a normal object, an item marked dead, an item marked deleted, or JSON null. These states represent different data conditions, and none should break a feed page.

狀態JSON 形狀建議處理方式
Live包含可用欄位的正常 item 物件正規化後呈現
Dead可能保留 item 中繼資料,但文字不可用顯示刪除提示,或省略內文
Deleteddeleted: true,通常含 type: "deleted" 與少量欄位隱藏內容,必要時保留 ID
Missing 或已清除JSON null從目前畫面移除,記錄 ID
暫時性上游錯誤非成功 HTTP 回應依有限次數重試,之後降級處理

Dead story 仍可能保留可解析的留言子項目。Deleted item 通常沒有足夠內容供呈現,因此 renderer 不得假設 titletexturlby 一定存在。若同一個服務也使用 Algolia 搜尋結果,應在轉換層統一處理缺少欄位與無法對應 Firebase item 的結果。

async function fetchWithRetry(path, attempts = 2) {
  let lastError;

  for (let attempt = 0; attempt < attempts; attempt += 1) {
    try {
      const response = await fetch(`${BASE}${path}`);

      if (!response.ok) {
        throw new Error(`HN returned ${response.status}`);
      }

      const value = await response.json();
      return value ?? null;
    } catch (error) {
      lastError = error;
      if (attempt + 1 < attempts) {
        await new Promise(resolve =>
          setTimeout(resolve, 250 * 2 ** attempt)
        );
      }
    }
  }

  throw lastError;
}

Python 先建立統一資料模型,可讓後續的 feed、搜尋結果與 AI agent 少寫防禦性判斷:

from dataclasses import dataclass
from typing import Any

@dataclass
class HackerNewsItem:
    id: int
    item_type: str
    title: str | None
    text: str | None
    dead: bool
    deleted: bool

    @classmethod
    def from_json(cls, raw: dict[str, Any]) -> "HackerNewsItem":
        return cls(
            id=raw["id"],
            item_type=raw.get("type", "deleted"),
            title=raw.get("title"),
            text=raw.get("text"),
            dead=bool(raw.get("dead", False)),
            deleted=bool(raw.get("deleted", False)),
        )

記錄 item ID、endpoint、狀態碼、重試次數、timeout 與 request correlation ID,也記下回應是 null、dead 還是 deleted。這些欄位能區分資料變動與上游故障,避免事故期間反覆抓取同一批失敗項目。

Building a Top Stories Feed End to End

A production feed separates ranking, retrieval, and presentation. Poll /topstories.json, persist the returned IDs, then hydrate only the stories needed for the current page. The Firebase API can return null for an item that disappeared, so filtering must happen before normalization.

async function buildTopFeed() {
  const ids = await getJson("/topstories.json");
  const rawItems = await mapPool(ids.slice(0, 30), id =>
    getJson(`/item/${id}.json`)
  );

  return rawItems
    .filter(item => item && item.type === "story" && !item.deleted && !item.dead)
    .map(item => ({
      id: item.id,
      title: item.title ?? "(untitled)",
      url: item.url ?? null,
      selfText: item.text ?? null,
      author: item.by ?? null,
      score: item.score ?? null,
      time: item.time ?? null,
      comments: item.descendants ?? 0,
      commentIds: item.kids ?? [],
    }));
}

One normalized record can drive a single-page application, server-rendered HTML, and RSS. Escaping titles and URLs for HTML, sanitizing HN text, and building links from the item ID keeps those outputs consistent. Algolia search results should pass through the same mapping layer, even though their field names and availability differ from Firebase records.

Comment expansion needs a boundary

The kids array contains child IDs, not complete comments. Recursive requests therefore need both a depth limit and a node budget. A busy thread can otherwise turn one render into an unexpectedly large request graph.

async function loadComments(ids, depth = 0, maxDepth = 2) {
  if (depth > maxDepth) return [];

  const items = await mapPool(ids, id =>
    fetchWithRetry(`/item/${id}.json`)
  );

  return items
    .filter(item => item && !item.deleted && !item.dead)
    .map(item => ({
      ...item,
      children: item.kids
        ? loadComments(item.kids, depth + 1, maxDepth)
        : Promise.resolve([]),
    }));
}

A six-step diagram illustrating the process of building a top stories feed using an API.

For serverless deployments, place the ID cache outside the request handler when the runtime allows it. Warm invocations can reuse the list until its refresh window expires, while a cold start fetches it again. Keep RSS generation deterministic, and derive pubDate from the item's Unix timestamp rather than the worker's fetch time.

Feeding an AI Agent with HN Data

An HN-powered agent should receive bounded tools, not an unrestricted dump of stories and comments. Give one tool responsibility for discovery through Algolia, and another for retrieving a Firebase thread. The model chooses between them based on the task.

A practical tool contract can stay small:

{
  "name": "hn_search",
  "description": "Search Hacker News stories and comments",
  "parameters": {
    "type": "object",
    "properties": {
      "query": { "type": "string" },
      "tags": { "type": "string" },
      "numericFilters": { "type": "string" }
    },
    "required": ["query"]
  }
}

Route search to Algolia and hydration to Firebase. The search response identifies candidates, while Firebase provides the current item structure and child IDs.

async function hnSearch({ query, tags = "story", numericFilters }) {
  const params = new URLSearchParams({
    query,
    tags,
    hitsPerPage: "20",
  });

  if (numericFilters) params.set("numericFilters", numericFilters);

  const response = await fetch(
    `https://hn.algolia.com/api/v1/search?${params}`
  );

  if (!response.ok) throw new Error(`HN Search failed: ${response.status}`);
  return response.json();
}

async function hnThreadSummarize({ id }) {
  const story = await fetchWithRetry(`/item/${id}.json`);
  if (!story) return null;

  const comments = await loadComments(story.kids ?? [], 0, 2);
  return { story, comments };
}

Require source preservation in the model prompt:

Summarize only the supplied HN content. For every material claim, include the HN item ID and URL. If the fetched context doesn't support an answer, say that the available context is insufficient.

Set retrieval limits in application code. Cap search hits, truncate long comment bodies, cache raw items, and stop before the context window is exceeded. A vector database for AI agent memory can preserve selected summaries, but fresh retrieval still determines current scores, comments, and feed membership.

Use search, hydrate, trim, cite as the request flow. Algolia finds candidates, Firebase supplies structured objects, the application removes irrelevant fields, and the model receives IDs and URLs with the text. Handle null, deleted items, and missing fields before prompt construction, so one incomplete HN record cannot invalidate the entire answer.

Quick Reference Card and Next Steps

Keep this card beside the codebase while debugging a feed.

EndpointMethodBase URLDefaultGotcha
Feed listsGEThttps://hacker-news.firebaseio.com/v0/Endpoint-specific ID arrayHydrate IDs individually
Item lookupGEThttps://hacker-news.firebaseio.com/v0/One item by IDResponse can be null or sparse
User lookupGEThttps://hacker-news.firebaseio.com/v0/One user by namesubmitted contains IDs, not objects
Archive cursorGEThttps://hacker-news.firebaseio.com/v0/Current maxitem valueUse as a cursor, not feed ranking
Full-text searchGEThttps://hn.algolia.com/api/v1/searchQuery plus indexed filtersIndex data isn't the Firebase object shape
Date searchGEThttps://hn.algolia.com/api/v1/search_by_datePage-based resultsPreserve page and filters
Search itemGEThttps://hn.algolia.com/api/v1/items/Indexed item by IDSearch result freshness can differ
Search userGEThttps://hn.algolia.com/api/v1/users/Indexed author lookupPaginate author results

For a first prototype, call Firebase directly and add a small in-memory cache. Monitor both the Firebase hostname and hn.algolia.com, recording upstream status and latency. Keep the official Hacker News API repository bookmarked for schema details and implementation notes.

Use Firebase for live item structure and Algolia for search, filtering, and analytics. Normalize both responses into an internal schema, because Algolia records are indexed documents rather than Firebase objects. Preserve item IDs and URLs so feed entries and AI-agent answers remain traceable.

Treat tombstones, deleted records, null responses, and missing fields as expected input. Cache raw Firebase items, cap search results, and avoid sending unnecessary fields to a model. A failed hydration should remove one entry, not break the entire feed.

Sokko helps teams run always-on AI agents and the devboxes that execute repository workflows, so an HN-powered agent can search discussions, build a feed, and publish a preview for review. Visit Sokko to deploy an agent with an isolated machine, live logs, persistent memory options, and a real preview URL.