GitHub's Billed Amount Is a Calendar Month. A Rolling 30-Day Net Is a Different Number.

I spent a week convinced our GitHub dashboard was wrong because it would not match the billing UI. Finance had billed amount. I had a 30-day net. Both were labeled "this month" in Slack, and that is on me.
GitHub's billing page is a UTC calendar month. First of the month 00:00 UTC to first of the next month 00:00 UTC. A rolling 30-day window on the 16th still has half of last month in it. If last month was noisy, your dashboard looks high. If this month started with a Copilot overage, GitHub looks high. Nobody is cooking the books.
Here is the thing: once I stopped treating those two numbers as a checksum, the remaining gaps were actually interesting. Net versus gross. Empty organizationName rows that are enterprise-level charges, not missing data. And the usage feed that will double-count Copilot if you sum billing + premium requests + AI credits.
I built the GitHub spend views in Unsave, the cloud cost and security tool I build, so this is the code we ended up with, not a thought experiment.
The Window GitHub Actually Bills
GitHub usage dates are UTC. Copilot's included AI credits reset at 00:00:00 UTC on the first of the calendar month. The billing UI follows the same cut. If your dashboard uses local time or a rolling length, you are not looking at the invoice.
I used days=0 as the sentinel for "this month" because zero cannot be a rolling length. 7|30|90|180|365 stay rolling.
export const THIS_MONTH = 0;
export const ROLLING_SPEND_DAYS = [7, 30, 90, 180, 365] as const;
export function spendDateFilter(days: number, now = new Date()): { gte: Date; lt?: Date } {
if (days === THIS_MONTH) {
const y = now.getUTCFullYear();
const m = now.getUTCMonth();
return {
gte: new Date(Date.UTC(y, m, 1)),
lt: new Date(Date.UTC(y, m + 1, 1)),
};
}
return { gte: new Date(now.getTime() - days * 86_400_000) };
}
The important part is lt on the first of next month. A rolling window is open-ended (gte only). A calendar month has to close or August will slowly eat September as the process stays up.
Gotcha:
new Date(y, m, 1)is local time. GitHub is UTC. UseDate.UTCor you will split a day on whatever offset the server happens to have.
If you are calling GitHub yourself, the usage API is the same feed the UI rolls up:
# Enterprise billing usage for the connected enterprise
# Requires a token with manage_billing:enterprise
curl -sS \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/enterprises/${GITHUB_ENTERPRISE}/settings/billing/usage"
Do not eyeball that JSON against "Last 30 days" in your own UI and then argue with billed amount. Filter each usage item's date field to [monthStart, nextMonthStart) first.
This is the same discipline I want in Azure too. How to learn Azure in 2027 is a longer argument about not confusing a demo with production. A rolling cost chart is a demo. An invoice is production.
Net Versus Gross, and Why Copilot Double-Counts
GitHub's UI shows Billed amount (net) and Gross amount. Discounts and included usage live in the gap. If you compare your net to their gross, you will "find" a bug that is just a label.
The quieter bug is usage kind. GitHub exposes billing usage, premium-request usage, and AI-credit usage. The last two are detail views of usage the billing feed already priced. I summed all three on the first pass. The total looked confident and did not match the invoice.
/**
* Only billing rows count toward spend.
*
* premium_request and ai_credit are detail views of usage the billing feed
* already bills for under its copilot lines. Summing all three usageKinds
* double-counts AI spend and the total silently stops matching the invoice.
*/
const SPEND_FILTER = { usageKind: "billing" as const };
Keep premium-request and AI-credit around for a Copilot explorer. Do not add them to the headline.
As of 1 June 2026, Copilot is usage-based: Business still $19/user with 1,900 included AI credits, Enterprise $39 with 3,900. Credits pool at the billing entity and reset on the first of the month. Unused seats still cost the seat line. That is a different post. This one is just: do not mix credit detail into billed amount.
Pro tip: Put the window label in the UI next to the number. "Net · This month" versus "Net · Last 30 days" kills more Slack threads than another chart.
Empty organizationName Is Enterprise-Level Spend
The usage API returns rows with a blank organization. I treated those as corrupt the first week. They are charges GitHub bills to the enterprise rather than to one org. On the enterprise I was looking at, that bucket was about a fifth of the bill.
If you drop those rows, every org looks fine and the headline is short. If you stuff them into a fake org called "unknown," every org roll-up is a lie. Surface them as their own figure so sum(byOrg) + unattributed = totalNet.
export const UNATTRIBUTED_LABEL = "Unattributed";
export interface GithubSpendSummary {
totalNet: number;
totalGross: number;
totalDiscount: number;
byOrg: Array<{ orgLogin: string; net: number }>;
/** Spend GitHub bills to the enterprise rather than to any one org. */
unattributedNet: number;
orgsWithNoSpend: string[];
windowDays: number;
}
In the UI we show that as Enterprise-level, not Unattributed, because "unattributed" sounds like we lost the join. We did not lose the join. GitHub never had an org on the row.
Warning: An org-scoped token will never see enterprise-level rows. If finance is looking at the enterprise invoice and you connected one org, you will not reconcile. That is a scope problem, not a date problem.
Same class of mistake as assuming a single-subscription Azure demo is the estate. I wrote about that gap in cloud labs don't prepare you for real work. One org's billed amount is a lab. The enterprise invoice is the job.
How I Actually Reconcile Now
I do this in order, and I stop when it matches.
- Window = this calendar month, UTC, closed at next month start.
- Net versus billed amount, gross versus gross amount.
- Same billing entity. Enterprise token versus org token.
- Add enterprise-level to the org table.
- Billing usage only. No premium-request, no AI-credit in the sum.
If it still disagrees, the GitHub UI is probably filtered (cost center, SKU, product). The API is not.
I also keep a rolling 30-day view. I just refuse to call it the invoice. Run-rate and invoice are both useful. They are not a checksum of each other.
The Unsave GitHub overview defaults to This month for that reason. I shipped that after losing the argument with myself.
If you want the product-shaped version of this, it lives on Unsave. I still think the code above is the part worth stealing even if you never sign in.
The Bigger Lesson
Dashboards lie when the label is friendlier than the query. "This month" means calendar month to finance and rolling window to every metrics library I have used. GitHub picked finance. Once you pick one and write it on the chip, the rest of the billed-amount fight turns into real bugs: double-counted Copilot, blank orgs, the wrong token scope.
Name the window. Close the month. Sum only the feed that invoices. Everything after that is actual engineering.



