GitHub Star History API: curl, Python, CSV and Chart Guide

Quick answer: GitHub’s new privacy-safe endpoint is GET /repos/{owner}/{repo}/stargazers/history. It returns aggregate stars by calendar week plus a seven-value daily breakdown, without exposing individual stargazer identities. Public repositories can be queried without authentication; fine-grained tokens for other accessible repositories need Metadata: read permission.[1][2]

Use this first:

OWNER="NousResearch"
REPO="hermes-agent"

curl -L \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  "https://api.github.com/repos/$OWNER/$REPO/stargazers/history?per_page=3"

A response item looks like this:

{
  "week": 1754784000,
  "total": 19,
  "days": [0, 12, 7, 0, 0, 0, 0]
}

week is a Unix timestamp, total is the number of stars added during that week, and days contains daily additions from Sunday through Saturday. Results are newest first.[2]

What changed and who needs to migrate

GitHub previously restricted endpoints that list individual stargazers to repository administrators and collaborators to reduce privacy abuse. The new history endpoint restores the legitimate analytics use case—tracking growth over time—while returning counts rather than identities.[1][3]

Migrate if your dashboard, README chart, internal report, launch tracker or open-source analytics tool currently:

  • downloads every stargazer with GET /repos/{owner}/{repo}/stargazers;
  • requests the application/vnd.github.star+json media type solely to collect starred_at values;
  • fails for repositories where the token owner is not an administrator or collaborator;
  • stops producing a complete curve for large repositories; or
  • stores usernames, avatars or profile URLs even though only trend data is required.

Keep the older endpoint only when your authorized workflow genuinely needs individual stargazer records. For charts and growth reports, the aggregate endpoint is the safer fit.

Endpoint reference

Item Value
History endpoint GET /repos/{owner}/{repo}/stargazers/history
Current count endpoint GET /repos/{owner}/{repo}/stargazers/count
Sort order Most recent week first
days order Sunday through Saturday
Maximum per_page 30 weeks
Maximum page 100
Public repository Can be used without authentication
Fine-grained token permission Repository Metadata: read
Successful response HTTP 200
Validation or abuse response HTTP 422

GitHub says pages move backward toward repository creation, zero-star weeks remain in the series, and week/day boundaries are not guaranteed to align with UTC.[2] Do not silently label the bucket boundary “UTC Sunday” in a dashboard.

Get the current star count

The companion endpoint returns the repository’s current count:

curl -L \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  "https://api.github.com/repos/NousResearch/hermes-agent/stargazers/count"

Response shape:

{
  "count": 12345
}

The number above is illustrative; query the API for the live count. The history endpoint gives period additions, while the count endpoint gives the current total.[2][4]

Convert one page to daily CSV with jq

This command expands every seven-day array into dated rows. It uses the returned week timestamp as the starting point and does not make a stronger timezone promise than GitHub does.

curl -sL \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  "https://api.github.com/repos/NousResearch/hermes-agent/stargazers/history?per_page=30" \
| jq -r '
  ["bucket_date","stars_added"],
  (.[] as $w
    | range(0; 7) as $i
    | [($w.week + ($i * 86400) | strftime("%Y-%m-%d")), $w.days[$i]])
  | @csv
' > github-star-history.csv

Check the file:

head github-star-history.csv

If the repository is older than 30 weeks, this captures only the first page. Use the complete Python downloader below for all available pages.

Complete Python downloader: pagination, validation and CSV

This standard-library script follows GitHub’s Link header until no next page remains, validates each weekly item, expands the daily arrays, sorts old-to-new and writes a CSV. Authentication is optional for public repositories.

#!/usr/bin/env python3
import csv
import json
import os
import sys
import urllib.request
from datetime import datetime, timedelta, timezone

OWNER = sys.argv<a href="https://github.blog/changelog/2026-09-04-new-api-endpoint-provides-privacy-safe-star-history-data" rel="noopener noreferrer">[1]</a> if len(sys.argv) > 1 else "NousResearch"
REPO = sys.argv<a href="https://docs.github.com/en/rest/activity/starring?apiVersion=2026-03-10" rel="noopener noreferrer">[2]</a> if len(sys.argv) > 2 else "hermes-agent"
TOKEN = os.getenv("GITHUB_TOKEN")

url = (
    f"https://api.github.com/repos/{OWNER}/{REPO}/"
    "stargazers/history?per_page=30&page=1"
)
headers = {
    "Accept": "application/vnd.github+json",
    "X-GitHub-Api-Version": "2026-03-10",
    "User-Agent": "github-star-history-export/1.0",
}
if TOKEN:
    headers["Authorization"] = f"Bearer {TOKEN}"

weeks = []
while url:
    request = urllib.request.Request(url, headers=headers)
    with urllib.request.urlopen(request, timeout=30) as response:
        page = json.load(response)
        if not isinstance(page, list):
            raise RuntimeError("Unexpected GitHub response: expected a list")
        weeks.extend(page)
        links = {}
        for part in response.headers.get("Link", "").split(","):
            if 'rel="' not in part:
                continue
            target, rel = part.split(";", 1)
            links[rel.split('"')<a href="https://github.blog/changelog/2026-09-04-new-api-endpoint-provides-privacy-safe-star-history-data" rel="noopener noreferrer">[1]</a>] = target.strip()[1:-1]
        url = links.get("next")

rows = []
today = datetime.now(timezone.utc).date()
for item in weeks:
    days = item.get("days")
    total = item.get("total")
    if not isinstance(days, list) or len(days) != 7:
        raise RuntimeError(f"Invalid days array: {item!r}")
    if sum(days) != total:
        raise RuntimeError(f"Weekly total does not match daily values: {item!r}")
    start = datetime.fromtimestamp(item["week"], timezone.utc)
    for offset, stars_added in enumerate(days):
        bucket_date = (start + timedelta(days=offset)).date()
        # The current weekly bucket can contain future zero-value slots.
        if bucket_date > today:
            continue
        rows.append((bucket_date.isoformat(), stars_added))

rows.sort(key=lambda row: row[0])
running_total = 0
with open("github-star-history.csv", "w", newline="", encoding="utf-8") as file:
    writer = csv.writer(file)
    writer.writerow(["bucket_date", "stars_added", "running_total"])
    for bucket_date, stars_added in rows:
        running_total += stars_added
        writer.writerow([bucket_date, stars_added, running_total])

print(f"Wrote {len(rows)} daily rows for {OWNER}/{REPO}")

Run it with:

python3 export_star_history.py NousResearch hermes-agent

For a repository that requires authentication, keep the token out of the file:

export GITHUB_TOKEN="YOUR_FINE_GRAINED_TOKEN"
python3 export_star_history.py OWNER REPO
unset GITHUB_TOKEN

Use the narrowest repository access possible and grant only the permission the endpoint requires: Metadata: read.[2]

Important cumulative-total caveat

The script’s running_total is the sum of every returned history bucket. It is complete only if you fetched the repository’s full available history. The current count endpoint is the authoritative way to retrieve the present count. Compare the final running total with /stargazers/count; if they differ, label the chart as partial rather than forcing the two figures to match.

JavaScript example for one page

This browser-or-Node example fetches one page and checks the response before using it:

const owner = "NousResearch";
const repo = "hermes-agent";
const url = `https://api.github.com/repos/${owner}/${repo}/stargazers/history?per_page=30`;

const response = await fetch(url, {
  headers: {
    Accept: "application/vnd.github+json",
    "X-GitHub-Api-Version": "2026-03-10",
  },
});

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

const weeks = await response.json();
const points = weeks.flatMap((week) =>
  week.days.map((starsAdded, index) => ({
    timestamp: (week.week + index * 86400) * 1000,
    starsAdded,
  }))
);

points.sort((a, b) => a.timestamp - b.timestamp);
console.table(points.slice(-14));

For production, parse the response Link header or use a GitHub SDK pagination helper rather than assuming one page is enough.

Migration checklist for existing star charts

  1. Find the old call. Search for /stargazers, starred_at, vnd.github.star+json, and fixed loops over hundreds of pages.
  2. Confirm the real requirement. If the product needs counts over time rather than people, replace identity data with the history endpoint.
  3. Update the API version header. Use the version shown in GitHub’s current example: 2026-03-10.[2]
  4. Rebuild the parser. Read week, total, and seven days values instead of user and starred_at.
  5. Reverse for charts. The API returns newest first; most chart libraries expect oldest first.
  6. Follow pagination. Do not guess the final page. Use the Link header.
  7. Validate totals. Confirm sum(days) == total for each item and compare the full series with the current count endpoint.
  8. Remove stored identity fields. Delete usernames, avatars and profile URLs from caches if the product no longer has a valid need for them.
  9. Test zero weeks. GitHub includes weeks with zero stars, so preserve them to avoid false visual jumps.[2]
  10. Label time correctly. Because boundaries are not guaranteed to align with UTC, call them GitHub buckets unless your own transformation has been verified.[2]
  11. Handle rate limits and errors. Record HTTP status and rate-limit headers; back off rather than retrying 422 responses aggressively.
  12. Run a before/after comparison. Compare several known repositories, including an old project, a new project and a repository with more than 40,000 stars.

Common mistakes

Mistake 1: Treating total as cumulative stars

total is the number added in that week, not the repository’s lifetime total. Build a cumulative series only after sorting the full data oldest-to-newest.[2][4]

Mistake 2: Reading the first days value as Monday

GitHub documents Sunday as the first position. The order is Sunday, Monday, Tuesday, Wednesday, Thursday, Friday and Saturday.[2]

Mistake 3: Assuming one page is complete

The default and maximum page size is 30 weeks, and pages proceed backward. Long-lived repositories require pagination.[2]

Mistake 4: Sending a powerful token for public data

Public resources can be requested without authentication. If authentication is needed, a fine-grained token with Metadata read access is sufficient for this endpoint.[2]

Mistake 5: Publishing exact user identities from the aggregate API

The endpoint does not identify who starred a repository. It cannot support user-level outreach, attribution or audience profiling—and that is intentional.[1][3]

Practical uses for the new data

  • annotate release dates against weekly star gains;
  • compare launch momentum across open-source projects;
  • rebuild README star-growth charts;
  • alert maintainers when growth changes sharply;
  • export privacy-safer trend data to a spreadsheet or warehouse;
  • measure whether documentation, demos or announcements coincide with increased interest; and
  • audit old integrations that collected more personal data than their charts required.

A star is only an interest signal. Do not present star growth alone as proof of active usage, revenue, quality or security.

FAQ

Does the GitHub Star History API expose usernames?

No. It returns aggregate weekly and daily counts without individual stargazer identities.[1][3]

What is the exact star history endpoint?

GET /repos/{owner}/{repo}/stargazers/history.[2]

Can I use it without a GitHub token?

Yes for public resources. For other repositories you can access, GitHub lists Metadata repository permission with read access for supported fine-grained token types.[2]

How many weeks can one response contain?

Up to 30. The page number can go up to 100.[2]

Is the days array Monday-first?

No. It begins with Sunday and has seven daily counts.[2]

Are week timestamps guaranteed to be UTC boundaries?

No. GitHub explicitly says week and day boundaries are not guaranteed to align with UTC.[2]

How do I get the live total number of stars?

Call GET /repos/{owner}/{repo}/stargazers/count. Do not assume the first history item contains the lifetime total.[2][4]

Why did older star-history integrations break?

GitHub restricted endpoints that expose individual stargazer and watcher data to address abuse. The aggregate history endpoint was introduced after community feedback to restore count-and-trend workflows without restoring identity exposure.[1][3]

Sources

[1] https://github.blog/changelog/2026-09-04-new-api-endpoint-provides-privacy-safe-star-history-data — New API endpoint provides privacy-safe star history data
[2] https://docs.github.com/en/rest/activity/starring?apiVersion=2026-03-10 — REST API endpoints for starring
[3] https://github.com/orgs/community/discussions/206104 — GitHub Community: privacy-safe star history API
[4] https://www.star-history.com/blog/new-github-star-history-api — The New GitHub Star History API

Leave a Comment

muddaser logo

Public Speaker, Softskills trainer and technology enthusiast

Contact

Muddaser Altaf

Social Address