The data API
Tamil Nadu's top three issues of the day as JSON: the counted split, grouped voices, checked facts, the research with every source, and the AI's tracked stance. Searchable by meaning, and readable as it stood on any day.
Only published issues appear, the same ones as on this site. It is free, with fair limits.
Quickstart
1. Create an account and make a key on the Keys page. 2. Call the API with it:
curl http://politic.in/v1/issues?limit=1 \ -H "authorization: Bearer ptn_your_key"
const res = await fetch("http://politic.in/v1/issues?limit=1", {
headers: { authorization: `Bearer ${process.env.POLITIC_KEY}` },
});
const { issues } = await res.json();
const voices = await fetch(`http://politic.in/v1/issues/${issues[0].id}/voices`, {
headers: { authorization: `Bearer ${process.env.POLITIC_KEY}` },
}).then((r) => r.json());import os, requests
h = {"authorization": f"Bearer {os.environ['POLITIC_KEY']}"}
issue = requests.get("http://politic.in/v1/issues", params={"limit": 1}, headers=h).json()["issues"][0]
take = requests.get(f"http://politic.in/v1/issues/{issue['id']}/take", headers=h).json()Keys
Send your key as a bearer token: authorization: Bearer ptn_…. A key is shown once when you make it; we keep only its hash. Make one key per app so you can revoke one without touching the others. An account can have up to 10 keys.
Limits
Everything is free. To keep it that way for everyone, requests are limited:
| Per key | 60 requests a minute |
| Per account | 10,000 requests a day across all its keys (India time; a key made without an account has its own) |
| Search | 20 searches a minute per key, on top of the above |
| The website | 120 pages and 20 searches a minute per address; no key needed |
Every response carries x-ratelimit-limit, x-ratelimit-remaining (both for the day) and x-ratelimit-reset (when the day's allowance starts again, in Unix seconds). Past a limit you get HTTP 429 with retry-after. Need more? Ask for a higher daily limit from the Limits page of your dashboard.
Going back in time
Nothing published is ever overwritten. A correction or an update to an issue is a new version, and every fact, voice, finding, take and stance is kept with the moment it became current and the moment it was replaced. So every read endpoint takes as_of, and answers with what was published and current then: as_of=2026-10-20 is the end of 20 October in India, and an ISO timestamp works too.
A snapshot is a day: /v1/snapshots lists them, /v1/snapshots/:date gives that day's issues, every position current then and the day's trends, and its as_of searches that snapshot in /v1/search.
# What did it know about the tariff on 20 October, and what does it know now? curl "http://politic.in/v1/search?q=electricity+tariff&as_of=2026-10-20" -H "authorization: Bearer ptn_your_key" curl "http://politic.in/v1/search?q=electricity+tariff" -H "authorization: Bearer ptn_your_key"
Errors
| 401 | No key, or the key is wrong or revoked. { "error": "invalid API key" } |
| 404 | No published issue with that id, or that part of it is not ready. |
| 400 | A parameter is wrong; the body says which. { "error": "as_of must be a date (YYYY-MM-DD, India time) or an ISO timestamp" } |
| 429 | Too many requests in a minute, or the day's limit is used up. retry-after says when to try again. |
| 500 | Our fault. Try again; the body carries a request_id to quote if it keeps happening. |
Published issues, newest day first and by rank within a day (up to three a day).
| limit | 1 to 100, default 20 |
| date | YYYY-MM-DD: one day's issues |
| before | YYYY-MM-DD: only issues before this date, for paging back |
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "as_of": "2026-10-25T10:00:00.000Z",
"issues": [ { "id": "iss_…", "date": "2026-10-25", "rank": 1, "day": 12, "version": 1,
"published_at": "…", "current_until": null, "title": { "en": "…", "ta": "…" },
"question": "…", "topic": "Electricity tariff", "consistency": "refined" } ],
"more": false }One issue as it stood at a moment (or ?version=), with its reel once posted.
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
| version | a version number from /versions, instead of as_of |
{ "id": "iss_…", "date": "2026-10-25", "rank": 1, "version": 2, "title": { "en": "…", "ta": "…" },
"summary": { "en": "…" }, "trend": "…", "keywords": ["…"],
"reel": { "instagram": "https://www.instagram.com/reel/…", "duration": 58.2 } }Every published version of an issue: a correction or an update is a new version, and the old one stays readable.
{ "issue_id": "iss_…", "versions": [ { "version": 2, "published_at": "…", "until": null, "items": 512 },
{ "version": 1, "published_at": "…", "until": "…", "items": 498 } ] }How different kinds of people are talking: the counted answer to the core question, grouped voices with mood and share, counts by speaker, camp and emotion, and the public figures who spoke, with links.
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "opinion": { "question_en": "…", "n": 412, "for": 31, "against": 52, "mixed": 9, "unclear": 8 },
"summary": { "mood": { "en": "…", "ta": "…" }, "undercurrent": "…", "quiet": "…",
"clusters": [ { "label_en": "…", "label_ta": "…", "share": 38, "emotion": "angry",
"position": "against", "says_en": "…", "says_ta": "…" } ] },
"counts": [ { "speaker": "ordinary_public", "camp": "DMK", "emotion": "angry", "position": "against", "n": 57 } ],
"public_figures": [ { "handle": "…", "speaker": "politician", "argument_en": "…", "post": "https://x.com/…" } ] }The AI's take, its reasons, what would change it, and the objections each side would raise; with how it stands against the AI's earlier position.
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "take": { "take_en": "…", "take_ta": "…", "reasons_en": ["…"], "would_change_en": "…",
"confidence": 0.7, "objections": [ { "camp": "…", "objection_en": "…" } ] },
"position": { "id": "pos_…", "topic": "…", "consistency": "refined", "change_reason": "…", "previous_position_id": "pos_…" },
"note": "An AI model's take, reviewed for fairness; not an endorsement." }What happened in plain sentences, the claims checked against sources, and the unproven ones going round.
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "what_happened_en": "…", "what_happened_ta": "…", "question_en": "…",
"verified": [ { "claim_en": "…", "source_url": "https://…" } ],
"disputed": [ { "claim_en": "…", "why_en": "…" } ] }The deep research behind the issue: a summary, the timeline, findings with their confidence, past positions, the numbers and a verdict on each claim. Every item carries the source it was read in; sources that stopped answering are dropped.
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "summary_en": "…", "timeline": [ { "date": "2026-10-20", "event_en": "…", "source_url": "https://…" } ],
"findings": [ { "angle": "history", "claim_en": "…", "evidence_en": "…", "source_url": "…",
"source_name": "…", "published": "2026-10-21", "confidence": "confirmed" } ],
"claims_checked": [ { "claim_en": "…", "verdict": "misleading", "why_en": "…", "source_url": "…" } ] }The days you can go back to, newest first, each with how many issues it had and its study day.
| limit | 1 to 365, default 30 |
| before | YYYY-MM-DD, for paging back |
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "snapshots": [ { "date": "2026-10-25", "issues": 3, "day": 12, "as_of": "2026-10-25T18:29:59.999Z", "url": "/v1/snapshots/2026-10-25" } ] }Everything as it stood at the end of a day in India: that day's issues, the AI's latest position on every topic then, the day's X trends, and the size of the record. Its as_of works in every other endpoint.
{ "date": "2026-10-20", "as_of": "2026-10-20T18:29:59.999Z",
"issues": [ { "id": "iss_…", "rank": 1, "title": { "en": "…" }, … } ],
"stances": [ { "topic": "NEET exemption", "issue_id": "iss_…", "date": "2026-10-18", "stance": { "en": "…" }, "consistency": "consistent" } ],
"trends": [ { "name": "#…", "query": "…", "best_rank": 2, "max_posts": 18400, "sightings": 12 } ],
"totals": { "issues": 33, "days": 11, "voices": 5120, "stances": 33, "items": 6812 } }Semantic search across everything published (one of: issue, fact, disputed, opinion, mood, voice_cluster, voice, summary, event, finding, past_position, number, claim_check, open_question, take, stance), in Tamil or English, as of a moment. Results carry a cosine score, the issue and version they belong to, and when they were valid. Searches have their own limit of 20 a minute.
| q | the question, up to 500 characters |
| kind | comma separated kinds, e.g. finding,claim_check |
| issue | keep one issue |
| limit | 1 to 50, default 10 |
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "q": "who opposed the tariff in 2022", "as_of": "…",
"results": [ { "id": "kn_…", "kind": "past_position", "text": "DMK, 2022: …", "data": { "who": "DMK", "when": "2022" },
"source_url": "https://…", "issue": { "id": "iss_…", "version": 1, "date": "2026-10-20", "title": { "en": "…" } },
"valid_from": "…", "valid_to": null, "score": 0.7123 } ] }The same store as rows, filtered and paged, newest first: every fact, voice, finding, take and stance valid at a moment.
| kind | comma separated kinds |
| issue | one issue's rows |
| date | the issues of one day |
| topic | stances on one topic |
| limit | 1 to 200, default 50 |
| cursor | next from the page before |
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "items": [ { "id": "kn_…", "kind": "claim_check", "text": "…", "data": { "verdict": "false", … }, … } ], "next": "kn_…" }The AI's position on every published issue, as of a moment, and whether each one held, sharpened, moved or reversed an earlier one.
| topic | follow one underlying question, e.g. NEET exemption |
| limit | 1 to 200, default 50 |
| as_of | read it as it stood at a moment: a date (YYYY-MM-DD, the end of that day in India) or an ISO timestamp; now by default |
{ "topics": [ { "topic": "…", "n": 3 } ],
"stances": [ { "id": "pos_…", "issue_id": "iss_…", "date": "2026-10-25", "topic": "…",
"stance": { "en": "…", "ta": "…" }, "confidence": 0.7, "consistency": "consistent",
"previous_position_id": "pos_…", "change_reason": null } ] }What X showed as trending for Tamil Nadu on a day, most persistent first: how often and how high each trend showed, and its post count.
| date | YYYY-MM-DD, today by default |
| limit | 1 to 100, default 30 |
{ "date": "2026-10-25", "trends": [ { "name": "#…", "query": "…", "context": "Trending in India", "best_rank": 1, "max_posts": 18400, "sightings": 14, "first_seen": "…", "last_seen": "…" } ] }One trend across days.
| name | the trend's name, any case |
| from | YYYY-MM-DD, 30 days before to by default |
| to | YYYY-MM-DD, today by default |
{ "name": "#…", "from": "2026-09-25", "to": "2026-10-25", "days": [ { "date": "2026-10-24", "sightings": 9, "best_rank": 3, … } ] }Your key's requests by day and endpoint, and your limits.
{ "usage": [ { "day": "2026-10-25", "route": "/v1/issues", "count": 12 } ], "daily_limit": 10000, "per_minute": 60, "search_per_minute": 20 }Webhooks
Add an endpoint on the Webhooks page and we POST to it the moment an issue is published (issue.published) and when a later version of it is (issue.updated), so you don't have to poll. Each request carries politic-event and a signature you should check before trusting the body.
{
"event": "issue.published",
"at": "2026-10-25T13:05:11.000Z",
"issue": {
"id": "iss_…", "date": "2026-10-25", "rank": 1, "version": 1, "published_at": "…",
"title": { "en": "…", "ta": "…" }, "summary": { "en": "…" }, "question": "…", "trend": "…",
"url": "http://politic.in/issues/iss_…", "api": "http://politic.in/v1/issues/iss_…",
"reel": { "instagram": "https://www.instagram.com/reel/…", "video": "http://politic.in/media/video/…/video.mp4" }
}
}Checking the signature
The header is politic-signature: t=1761397511,v1=5f1c…: the HMAC-SHA256 of <t>.<raw body> with your endpoint's secret, in hex. Compare in constant time and refuse a t more than five minutes old.
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const want = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1 ?? ""));
}Answer with any 2xx within ten seconds. A delivery that fails is logged on your Webhooks page and not retried; the issue is always in the API.
Using the data
The take is an AI model's reading, reviewed for fairness: label it as that, not as fact or endorsement. Post text from X is never served: voices are paraphrased, ordinary users appear only as counts, and only public figures are named, with a link to their post. Credit politic.tamilnadu and link the issue when you republish.