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+jsonmedia type solely to collectstarred_atvalues; - 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
- Find the old call. Search for
/stargazers,starred_at,vnd.github.star+json, and fixed loops over hundreds of pages. - Confirm the real requirement. If the product needs counts over time rather than people, replace identity data with the history endpoint.
- Update the API version header. Use the version shown in GitHub’s current example:
2026-03-10.[2] - Rebuild the parser. Read
week,total, and sevendaysvalues instead ofuserandstarred_at. - Reverse for charts. The API returns newest first; most chart libraries expect oldest first.
- Follow pagination. Do not guess the final page. Use the
Linkheader. - Validate totals. Confirm
sum(days) == totalfor each item and compare the full series with the current count endpoint. - Remove stored identity fields. Delete usernames, avatars and profile URLs from caches if the product no longer has a valid need for them.
- Test zero weeks. GitHub includes weeks with zero stars, so preserve them to avoid false visual jumps.[2]
- Label time correctly. Because boundaries are not guaranteed to align with UTC, call them GitHub buckets unless your own transformation has been verified.[2]
- Handle rate limits and errors. Record HTTP status and rate-limit headers; back off rather than retrying
422responses aggressively. - 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