The hardest question in SEO isn't "what's our score?" — it's "what changed?" You shipped a release, a freelancer touched the templates, the dev team rebuilt the marketing site on a new framework. Two weeks later traffic dips. Was it that, or something else?
If you've only got today's score, you can guess. If you've got every score for that URL going back 90 days, with the exact category that dropped and the timestamp of the change, you don't have to guess.
That's what historical comparison does on SEO Score API. It's now included on every paid plan ($5/mo and up), and this post walks through what it returns, how the retention windows work, and a few patterns we've seen agencies use it for.
What you get
Every paid /audit response now includes a history block:
{
"url": "https://example.com",
"score": 87,
"grade": "B+",
"history": {
"first_audit": false,
"audits_recorded": 12,
"previous_audit_at": 1746115200,
"days_since_last": 7.02,
"delta": {
"score": -3.5,
"grade_change": "A- → B+",
"categories": {
"seo": -1.0,
"performance": -4.5,
"accessibility": 0,
"ai_readability": 0.5
}
}
}
}
That's the delta block — the answer to "what changed since last time." On the very first audit of a URL, the block tells you that history will start showing up on the next run. On every subsequent audit, you get:
- The total score delta and grade change.
- A per-category breakdown so you can see which part of the audit moved.
- Days since the last audit, so a regression that happened over a weekend is obvious.
Two endpoints for the bigger picture
The delta on /audit is fine for one-off checks. For dashboards and SLA reports, you'll want the two dedicated endpoints.
GET /history?url=<url>
Returns the full timeseries for a single URL — every audit you've run, oldest first, plus a summary block:
{
"url": "https://example.com",
"count": 12,
"summary": {
"first_audit_at": 1738339200,
"latest_audit_at": 1746720000,
"first_score": 78,
"latest_score": 87,
"min_score": 71,
"max_score": 89,
"avg_score": 83.4,
"total_delta": 9.0
},
"history": [
{
"timestamp": 1738339200,
"score": 78,
"grade": "C+",
"categories": {"seo": 81, "performance": 65, "accessibility": 92, "ai_readability": 74},
"priority_count": 11
},
...
]
}
Use this to plot trend charts, build SLA reports, or feed an LLM that's writing a quarterly recap.
GET /history/domains
Lists every domain your key has ever audited, with the latest score and a 30-day trend. This is the endpoint to call when you want a one-shot "how is everything doing?" dashboard:
{
"domains": [
{
"domain": "example.com",
"audit_count": 47,
"first_audited": 1738339200,
"last_audited": 1746720000,
"latest_score": 87,
"latest_grade": "B+",
"trend_30d": -3.0
},
...
]
}
It's intentionally cheap — one call, one row per domain — so you can render an agency-wide "all clients at a glance" view without iterating.
Retention is tied to your plan
This is the part to read carefully if you're picking a tier:
| Plan | History retention |
|---|---|
| Free | None |
| Starter ($5/mo) | 30 days |
| Basic ($15/mo) | 90 days |
| Pro ($39/mo) | 1 year |
| Ultra ($99/mo) | Unlimited |
A few things worth knowing about how that's enforced:
- The cutoff is the request time, not the audit time. If you downgrade from Pro to Starter, you immediately stop seeing data older than 30 days — the rows aren't deleted on the spot, but the API filters them out. If you upgrade back later, anything still in the database becomes visible again (within your new window).
- Old rows get pruned eventually. Once a day, the cleanup job deletes audit rows older than each user's retention window. This bounds the database; it also means a downgrade-then-upgrade pattern won't recover data that's already been pruned. If you might need history later, stay on a plan that covers your window.
- Free and demo tiers see a gated message. The
historyblock in/auditreturns{"_gated": "..."}for free-tier keys and the dedicated endpoints return a 402. There's no silent failure — your code can branch on the gate cleanly.
Three patterns we've seen agencies build
1. Weekly digest of clients whose score moved
Pull /history/domains once a week, filter to anything where trend_30d is below -2.0, and post the list to Slack. That's your "things to look at this week" queue, generated automatically.
import os, requests
from collections import defaultdict
API_KEY = os.environ["SEOSCORE_API_KEY"]
r = requests.get(
"https://api.seoscoreapi.com/history/domains",
headers={"X-API-Key": API_KEY},
)
r.raise_for_status()
domains = r.json()["domains"]
dropped = [d for d in domains if d.get("trend_30d", 0) <= -2.0]
dropped.sort(key=lambda d: d["trend_30d"])
print(f"{len(dropped)} clients down 2+ points this month:")
for d in dropped:
print(f" {d['domain']}: {d['latest_score']} ({d['trend_30d']:+.1f} 30d)")
That's eight lines and it's the kind of report that used to take a half-day in a spreadsheet.
2. Per-category regression catcher in CI
If you're running /audit from a CI pipeline (we've got a GitHub Actions guide and a generic CI/CD guide) you can fail the build only when a specific category regresses, not just the overall score:
result = requests.get(f"https://api.seoscoreapi.com/audit?url={url}",
headers={"X-API-Key": API_KEY}).json()
delta = result.get("history", {}).get("delta", {})
cat_deltas = delta.get("categories", {})
if cat_deltas.get("performance", 0) <= -5:
print(f"::error::Performance dropped {cat_deltas['performance']:+.1f}")
sys.exit(1)
A 5-point drop in performance is much more actionable than a 1-point drop in the blended score, and you'd never see it without per-category history.
3. Quarterly SLA report, generated from /history
For agency retainers, the historical endpoint replaces the manual screenshot-and-paste-into-Google-Slides workflow. Pull 90 days of history per client URL, plot it with Matplotlib or render an HTML one-pager, and attach it to the next invoice. The agency monitor setup post has a full worked example using Northbeam Digital.
"But I've been auditing for months — is that data already there?"
If you've been on a paid plan and using the API, yes. We started recording every audit's full score breakdown back when the audit_scores table was added, and the new history features read straight from that table. The first time you call /history?url= on a URL you've already audited a few times, you'll see all of it (within your retention window).
If you've only been on free, you'll start fresh on whatever paid plan you upgrade to. The data simply wasn't being scoped to your key before.
Why this matters
Most SEO tools are good at telling you what's broken right now. They're bad at telling you whether the thing that's broken now was broken last month. That's the gap historical tracking closes — and it's the thing that turns an audit from a snapshot into a system of record.
If you want to try it: grab a Starter key for $5/mo, audit a URL twice, and you'll see the delta block on the second response. From there, the two history endpoints do the rest.