One GET returns the tracked AI-regulation records for a jurisdiction, each with its status, its dates, and the official .gov URL behind it. Here is the whole loop: find the slug, page through the records, handle the errors, and know exactly which caps your tier is under.
Published · AI Law Tracker Editorial Team
Every read endpoint works with no credentials at all. Anonymous callers are limited to 20 requests/minute and 500 requests/day per IP, capped at 5 records per page, and receive a trimmed field envelope. That is enough to evaluate the data and not much more.
A free key — self-serve, no card — raises you to 60 requests/minute, 2,000 requests/day and 60,000 requests/month, returns the complete record (summary, source, dates, in_force) and allows 25 records per page. Send it in either header form:
# Preferred
curl -s "https://ai-law-tracker.com/api/v1/health" -H "X-API-Key: alt_free_your_key_here"
# Equivalent
curl -s "https://ai-law-tracker.com/api/v1/health" -H "Authorization: Bearer alt_free_your_key_here"curl -X POST "https://ai-law-tracker.com/api/v1/keys" \
-H "Content-Type: application/json" \
-d '{"email":"you@company.com"}'The jurisdiction filter takes an exact slug, not a display name. GET /v1/states lists the US state jurisdictions with their record counts; GET /v1/jurisdictions covers everything including us-federal, eu and country slugs.
curl -s "https://ai-law-tracker.com/api/v1/states" -H "X-API-Key: alt_free_your_key_here"{
"api_version": "v1",
"data": [
{ "slug": "california", "name": "California", "abbr": "CA", "scope": "state", "count": 55 },
{ "slug": "colorado", "name": "Colorado", "abbr": "CO", "scope": "state", "count": 12 }
],
"meta": { "count": 50 }
}GET /v1/laws accepts scope, jurisdiction, status (case-insensitive substring), in_force, updated_since, q, sort (updated_at | record_date | title), order, limit and offset. Filters and sorting are honoured at every tier — only page size and pagination depth are tier-capped.
curl -s -G "https://ai-law-tracker.com/api/v1/laws" \
-H "X-API-Key: alt_free_your_key_here" \
--data-urlencode "jurisdiction=california" \
--data-urlencode "sort=record_date" \
--data-urlencode "order=desc" \
--data-urlencode "limit=25"{
"api_version": "v1",
"data": [
{
"id": "5f9c2b0a-1e34-4d21-9a77-0c1b2d3e4f56",
"scope": "state",
"jurisdiction": { "slug": "california", "name": "California", "abbr": "CA" },
"identifier": "SB 942",
"title": "California AI Transparency Act",
"record_type": "bill",
"status": "Signed — effective 2026-01-01",
"in_force": null,
"record_date": "2024-09-19T00:00:00Z",
"summary": "Requires covered providers to offer AI-detection tooling and to apply latent disclosures…",
"source": "leginfo.legislature.ca.gov",
"official_url": "https://leginfo.legislature.ca.gov/faces/billTextClient.xhtml?bill_id=202320240SB942",
"updated_at": "2026-07-05T14:02:11Z",
"checked_at": "2026-08-01T03:11:42Z",
"attribution": "Data by AI Law Tracker (CC BY 4.0) — https://ai-law-tracker.com"
}
],
"meta": {
"total": 55,
"count": 25,
"limit": 25,
"offset": 0,
"tier": "free",
"ai_scope": "ai_specific",
"gated": ["limit"],
"upgrade": "Free shows the full record quality but at low volume…"
}
}updated_at moves only on a real content diff; checked_at moves every time the source was successfully re-checked, even when nothing changed. Use updated_at for “what changed”, checked_at for “how fresh is this”.Page with limit and offset, and stop on meta.total — never on an assumption about page size. If you ask for more than your tier allows, the API does not error: it clamps the value, returns the smaller page, and tells you it did so in meta.gated. Read the echoed meta.limit rather than the one you sent.
import os
import time
import requests
BASE = "https://ai-law-tracker.com/api/v1"
SESSION = requests.Session()
SESSION.headers.update({"X-API-Key": os.environ["ALT_API_KEY"]})
def fetch_state_laws(slug, page_size=25):
"""Yield every tracked AI-law record for one jurisdiction slug."""
offset = 0
while True:
r = SESSION.get(
f"{BASE}/laws",
params={
"jurisdiction": slug,
"sort": "record_date",
"order": "desc",
"limit": page_size,
"offset": offset,
},
timeout=30,
)
if r.status_code == 429:
# Honour the server's own backoff rather than inventing one.
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
r.raise_for_status()
payload = r.json()
rows = payload["data"]
meta = payload["meta"]
for row in rows:
yield row
# meta.limit is what the server ACTUALLY applied after tier clamping.
offset += meta["limit"]
if not rows or offset >= meta["total"]:
return
if __name__ == "__main__":
for law in fetch_state_laws("california"):
print(law["identifier"], "|", law["status"], "|", law["official_url"])type LawRecord = {
id: string;
scope: 'state' | 'federal' | 'eu' | 'global';
jurisdiction: { slug: string; name: string | null; abbr: string | null };
identifier: string | null;
title: string | null;
record_type: string | null;
status: string | null;
in_force: boolean | null;
record_date: string | null;
summary: string | null;
source: string | null;
official_url: string | null;
updated_at: string | null;
checked_at: string | null;
attribution: string;
};
type ListResponse = {
api_version: string;
data: LawRecord[];
meta: { total: number; count: number; limit: number; offset: number; tier?: string; gated?: string[] };
};
const BASE = 'https://ai-law-tracker.com/api/v1';
export async function fetchStateLaws(slug: string, apiKey: string): Promise<LawRecord[]> {
const out: LawRecord[] = [];
let offset = 0;
for (;;) {
const url = new URL(`${BASE}/laws`);
url.searchParams.set('jurisdiction', slug);
url.searchParams.set('sort', 'record_date');
url.searchParams.set('order', 'desc');
url.searchParams.set('limit', '25');
url.searchParams.set('offset', String(offset));
const res = await fetch(url, { headers: { 'X-API-Key': apiKey } });
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') ?? '5');
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(`ALT ${res.status}: ${body?.error?.code ?? 'unknown'} — ${body?.error?.message ?? res.statusText}`);
}
const page = (await res.json()) as ListResponse;
out.push(...page.data);
offset += page.meta.limit;
if (page.data.length === 0 || offset >= page.meta.total) return out;
}
}Every failure uses one envelope, so a single handler covers all of them:
scope, an unparseable updated_since, or a missing q on /v1/search. The message names the offending parameter.Retry-After and error.retry_after both tell you how long to wait. Back off; do not retry tightly./v1/changes, /v1/feed, /v1/laws/{id}/history). The body names required_tier, so you can surface a precise message instead of a generic denial.meta.gated naming what was clamped and meta.upgrade explaining why.{
"api_version": "v1",
"error": {
"code": "rate_limited",
"message": "…",
"docs": "https://ai-law-tracker.com/developers",
"retry_after": 12
}
}When you want “everything about deepfakes in Texas” rather than “everything in Texas”, use GET /v1/search. It takes the same parameters as /v1/laws plus a required q, and defaults to newest-first by record_date.
curl -s -G "https://ai-law-tracker.com/api/v1/search" \
-H "X-API-Key: alt_free_your_key_here" \
--data-urlencode "q=deepfake" \
--data-urlencode "jurisdiction=texas" \
--data-urlencode "limit=25"/v1/statesUS state jurisdictions with record counts (the slug source)./v1/jurisdictionsEvery covered jurisdiction, filterable by scope./v1/lawsList records: scope, jurisdiction, status, in_force, updated_since, q, sort, order, limit, offset./v1/laws/{id}One record by uuid./v1/laws/{id}/sourcesThe tiered source URLs behind a record./v1/searchText search over title + summary. `q` required./v1/keysSelf-serve free key by email.Full reference in the developer docs. Plans and limits on pricing.
Yes for this workload. Anonymous access needs no key at all (20 req/min, 500 req/day, 5-record pages). A free key is self-serve with no card and gives 60 req/min, 2,000 req/day, 60,000 req/month, the full record envelope and 25-record pages. Paid plans exist for production volume, the change feed, history and webhooks.
Your tier clamped it. Free is capped at 25 records per page and anonymous at 5. The response is still a 200 — check meta.limit for what was applied and meta.gated for the list of clamped parameters. Developer and above allow the full 100.
By default it counts AI-specific records, which is echoed back as meta.ai_scope so the number is never ambiguous. It is the total matching your filters, not the size of the page — page with limit and offset until offset reaches total.
Yes, under CC BY 4.0 with attribution. Every record carries the attribution string in its own payload so you can propagate it automatically.
Data by AI Law Tracker, licensed CC BY 4.0 — attribution required: “Data by AI Law Tracker (CC BY 4.0) — https://ai-law-tracker.com”. AI Law Tracker provides informational data, not legal advice. Verify every record against its official source before relying on it.