GitHub Stats Card API: Cards That Stay Current
A GitHub stats card API renders a card as an image and hands the bytes straight back, so a README can point at a URL instead of a PNG somebody committed six months ago. You design the card once, mark the numbers dynamic, and send a different username or repo per call.
The rendering is the easy half. The half worth writing down is what happens after you paste that URL into a README, because GitHub doesn't fetch your image the way a browser does, and that changes both how fresh the card looks and how much of your allowance it spends.
If you only want one card for your own profile, the GitHub stats card generator and the repo language chart do it free in the browser, and the walkthrough for those covers the markdown embed. This post is for the case where you own 40 repos and want them all to look the same.
One design every repoOne design, every repo
Dimensions are fixed when you build a design and can't be overridden in a render call. For cards that's the right constraint: every repo's card comes out at the same size, so a README grid never breaks because one project has a longer name.
Mark the fields that change and leave everything else alone:
curl -X POST 'https://render.imejis.io/v1/YOUR_DESIGN_ID' \
-H 'dma-api-key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"repo_name": { "text": "acme/parser" },
"stars": { "text": "1,284" },
"language": { "text": "Rust" }
}' \
--output card.pngThe response is the image. No job id, no polling, nothing to wait on. Loop that over a repo list and you've got a whole org's cards in one script.
What github does to your image urlWhat GitHub does to your image URL
GitHub does not let a README load an image from your server directly. Every image in markdown is rewritten to run through Camo, GitHub's own image proxy, which fetches the file once and serves a copy from camo.githubusercontent.com. GitHub documents this behaviour under anonymized URLs, and the reason is privacy: the proxy stops your server from seeing the IP address of everyone who reads the README.
Two consequences follow, and both surprise people.
Your card refreshes on GitHub's timing, not yours. A render URL in a README is live in the sense that Camo will eventually re-fetch it. GitHub doesn't publish a cache lifetime, so treat the refresh as best-effort. If a number has to be right at a specific moment, render it on a schedule and commit the file.
Your view count and your render count are different numbers. Camo absorbs most of the traffic, so a README read a thousand times does not mean a thousand renders. It also means you can't predict the ratio, which matters for the next section.
A cache hit still costs a renderA cache hit still costs a render
This one is worth stating plainly because it's the opposite of what most people assume.
The render service does keep a cache, keyed on the design and the exact payload, and an identical request comes back from it without re-rendering. But the quota is consumed when the API key is checked, and that check runs before the cache lookup. So a cached response is faster and still costs one render.
The rule we'd give: budget on requests received, not on unique images produced. When you run out, the endpoint answers 429 with the numbers you need to handle it:
{
"success": false,
"message": "You have reached your quota limit",
"data": {
"usage": 100,
"limit": 100,
"remaining": 0,
"resetAt": "2026-10-01T00:00:00.000Z"
}
}Read resetAt and back off to a committed PNG until the period rolls over. A README that silently 429s shows a broken image to every visitor, which is worse than a card that's a week stale.
Three ways to put a card in a readmeThree ways to put a card in a README
| Approach | Freshness | Renders used | Best for |
|---|---|---|---|
| Commit a PNG | Whenever CI runs | One per CI run | Busy public repos, predictable cost |
| Embed a render URL | Camo's timing | One per Camo fetch | Personal profiles, low traffic |
Render with delivery=hosted | Whenever you re-render | One per render | Org dashboards, many readers, one source image |
Hosted delivery is the middle path people miss. Instead of the image bytes, the endpoint stores the render and answers with JSON:
{
"success": true,
"delivery": "hosted",
"url": "https://storage.googleapis.com/.../card.png",
"format": "png"
}Point the README at that permanent URL. Readers hit storage rather than the render endpoint, so a repo with 50,000 monthly readers costs exactly as many renders as you choose to trigger. The trade is that nothing refreshes until you re-render, which is a scheduling problem rather than a billing one.
What a github stats card api costsWhat a GitHub stats card API costs
As of September 2026 that's 100 free renders a month, no card. A nightly refresh of one card is about 30 renders a month, so a personal profile fits inside the free tier with room to spare.
An organization is a different sum. 40 repos refreshed nightly is 1,200 renders a month, which lands on Basic at $14.99. Refresh weekly instead and the same 40 repos cost about 170 a month, which is still Basic but with a lot of headroom. The pricing page has the full table, and render quota explains how the two counters work.
Wire it into ciWire it into CI
The scheduled-commit approach is a dozen lines and it makes the cost a fixed number you chose:
name: refresh-cards
on:
schedule:
- cron: "0 6 * * 1"
jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Render the card
run: |
curl -sS -X POST "https://render.imejis.io/v1/${{ secrets.DESIGN_ID }}" \
-H "dma-api-key: ${{ secrets.IMEJIS_KEY }}" \
-H "Content-Type: application/json" \
-d '{"repo_name":{"text":"${{ github.repository }}"}}' \
--output .github/card.png
- run: git add .github/card.png && git diff --cached --quiet || git commit -m "refresh card" && git pushWeekly, one render, and the README never shows a broken image because you hit a limit on a Tuesday.
Where to go nextWhere to go next
Build the card in the free generator until the layout is right, then move that same layout to a design and render it. If you want the numbers themselves to come from an agent rather than a script, MCP covers that path, and the API docs have tested calls in Node, Python, and Go.