Skip to content

Data · API

Data API

Statements, officials, bills, votes, surveys and violence as JSON

What it’s for

The lab’s data, one request at a time: classified statements, officials and their profiles, bills, roll-call votes, survey aggregates and political violence events.

Use the API to pull a slice (one official’s votes, a phrase in Democrats’ press releases since January, the bills of one state) or to keep a project up to date. For a whole dataset the bulk files are faster: one download each, CSV and Parquet in a zip. Every address starts with https://api.americaspoliticalpulse.com/v1, and the API only reads: every request is a GET.

Data API resources
PathWhat
/v1/statementsClassified statements: tweets, press releases, newsletters, floor speeches (members of Congress, state legislators, governors). Tweets carry tweet_id and an empty text.
/v1/officialsOfficials: members of Congress, state legislators, governors.
/v1/billsFederal and state bills with sponsors, stage and policy area.
/v1/votesRoll-call votes with party splits.
/v1/statements/{id}One statement
/v1/officials/{id}An official's full profile (rhetoric, policy, ideology, money, votes, legislation)
/v1/officials/{id}/votesAn official's roll-call votes, newest first
/v1/bills/{bill_id}/similarThe same proposal in other states (similarity 0.87 or more)
/v1/surveys/state-estimatesState opinion estimates (MRP)
/v1/surveys/toplinesSurvey topline reports
/v1/violence/eventsPolitical violence events
/v1/bulkBulk files (CSV and Parquet zips)
/v1/bulk/{name}Download a bulk file (302 to a five-minute link; counts toward the daily downloads)
/v1/usageYour limits and use
Notes
Survey data are aggregates only: the API never serves individual responses. Each row links to its entry in the reference.

Back to topNext: Account and key

Account and key

A free account gets you one key. Create an account to get a key.

Sign up with your email address, your name and institution, your sector and a line on what you plan to do with the data. There is no password: each time you sign in, we email you a link.

On your account page, make a key. It starts with pulse_ and is shown once, in full, so copy it then; we keep only a fingerprint of it. Making a new key replaces the old one, which stops working within a minute, and so does a key you revoke. Limits and counts belong to the account, not the key, so a new key starts with what you have already used today.

Back to topNext: Authentication

Authentication

Send your key in a header with every request, in either of two forms.

A key in the address (?api_key= or ?key=) is refused with 400. Only https://api.americaspoliticalpulse.com/v1 itself and the OpenAPI file answer without a key.

Headers
Authorization: Bearer pulse_…
X-API-Key: pulse_…

Back to topNext: First request

Your first request

One page of statements: Democrats’ statements with the phrase “school safety”, 100 at a time.

Keep the key in an environment variable (PULSE_KEY here), not in your code. The examples use curl, Python with requests, and R with httr2.

curl
curl -H "Authorization: Bearer $PULSE_KEY" \
  "https://api.americaspoliticalpulse.com/v1/statements?q=%22school%20safety%22&party=D&limit=100"
Python
import os
import requests

API = "https://api.americaspoliticalpulse.com/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['PULSE_KEY']}"

r = s.get(f"{API}/statements", params={"q": '"school safety"', "party": "D", "limit": 100})
r.raise_for_status()
page = r.json()
print(page["total"], len(page["data"]))
R
library(httr2)

page <- request("https://api.americaspoliticalpulse.com/v1/statements") |>
  req_auth_bearer_token(Sys.getenv("PULSE_KEY")) |>
  req_url_query(q = '"school safety"', party = "D", limit = 100) |>
  req_perform() |>
  resp_body_json()
page$total

Every response has the same four parts: data (a list, or one object), next_cursor for the next page, total (the number of matches, where a resource counts them) and meta with the license and the citation.

Posts on X come with their tweet_id and an empty text: X’s terms let us share post ids, not the posts. Press releases, newsletters and floor speeches come with their whole text.

Response
{
  "data": [ … up to 100 statements … ],
  "next_cursor": "…",
  "total": …,
  "meta": {
    "licence": "CC BY 4.0",
    "licence_url": "https://creativecommons.org/licenses/by/4.0/",
    "citation": "Westwood, S. J., & Lelkes, Y. (2024). America's Political Pulse [Data set]. https://americaspoliticalpulse.com/data",
    "sources": "https://americaspoliticalpulse.com/data#sources",
    "docs": "https://americaspoliticalpulse.com/api"
  }
}

Back to topNext: Filters and pages

Filters and pages

Narrow a list with filters, then follow next_cursor to the end.

The four lists (statements, officials, bills, votes) take the same paging parameters and filters of their own; the reference lists every one.

q takes words, “a phrase” in quotes, -word to leave a word out, a | b for either and prefix*; upper-case OR, AND and NOT work too. A filter that takes several values takes them comma-separated (party=D,R) or repeated (party=D&party=R), except policy_area, which must be repeated because area names contain commas. Party and state codes can be lower case, and dates are YYYY-MM-DD. A parameter the list doesn’t take is refused with 400, so a typo can’t quietly return everything.

limit sets the page size, 1 to 100 (25 if you leave it out). Send each page’s next_cursor back as cursor, with the other parameters unchanged, until it comes back null. A cursor belongs to its query: change a filter and it is refused with 400. A full page can be followed by one empty page. You can page as deep as the results go, but the index is rebuilt nightly, and a walk that spans the rebuild can skip or repeat items. An official’s votes (/v1/officials/{id}/votes) page the same way.

The example below collects every statement by Pennsylvania officials since January 2025 that matches “tariff”, waiting out the per-minute limit. At the default limits that is up to 6,000 statements a minute.

Python
import os
import time
import requests

API = "https://api.americaspoliticalpulse.com/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['PULSE_KEY']}"

params = {"q": "tariff", "state": "PA", "date_from": "2025-01-01", "limit": 100}
rows = []
while True:
    r = s.get(f"{API}/statements", params=params)
    if r.status_code == 429:
        wait = int(r.headers.get("Retry-After", "60"))
        if wait > 60:  # the day's limit: stop here, carry on tomorrow
            break
        time.sleep(wait)
        continue
    r.raise_for_status()
    page = r.json()
    rows += page["data"]
    if not page["next_cursor"]:
        break
    params["cursor"] = page["next_cursor"]

print(len(rows), "of", page["total"])
R
library(httr2)

req <- request("https://api.americaspoliticalpulse.com/v1/statements") |>
  req_auth_bearer_token(Sys.getenv("PULSE_KEY")) |>
  req_retry(max_tries = 5)  # waits out a 429 or 503, reading Retry-After

params <- list(q = "tariff", state = "PA", date_from = "2025-01-01", limit = 100)
rows <- list()
repeat {
  page <- req |> req_url_query(!!!params) |> req_perform() |> resp_body_json()
  rows <- c(rows, page$data)
  if (is.null(page$next_cursor)) break
  params$cursor <- page$next_cursor
}
length(rows)

Back to topNext: Limits

Limits and quotas

60 requests a minute and 10,000 a day for each account, and 100 file downloads a day, unless the lab has raised yours.

Days run on UTC. A request turned away for being over a limit doesn’t count.

Each successful response says where you stand, in X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute, X-RateLimit-Limit-Day and X-RateLimit-Remaining-Day. Over a limit you get 429 with Retry-After: the seconds to the next minute, or to midnight UTC. /v1/usage gives your limits, today’s requests by resource and the last 30 days.

Downloads count the site’s file downloads and /v1/bulk/{name} together; each answers with a link that works for five minutes. Separately, one address can send at most 1,000 requests to the API in five minutes, whatever keys it uses, and then gets 429 for a few minutes. Need more? Write to the lab.

Back to topNext: Errors

Errors

An error is JSON with one field, a sentence saying what went wrong: {"error": "limit must be 1 to 100"}.

Status codes
StatusWhen
400A parameter is wrong: a name the resource doesn’t take, a value it can’t read, a limit outside 1 to 100, a cursor from another query. Also a key sent in the address.
401No key, or a key that isn’t valid (revoked or mistyped).
403The account is suspended, or the API isn’t open to it yet.
404No such resource, statement, official, bill or bulk file, or no votes for that official and window.
405A method other than GET: the API is read-only.
429Over a limit. Retry-After gives the seconds to wait, except for the daily downloads (they reset at midnight UTC) and the per-address limit.
500Something failed on our side. Try again.
502The search service failed. Try again.
503A resource is being rebuilt, or the search service is busy (then with Retry-After: 5). Try again shortly.

Back to topNext: Versions

Versions and changes

This is version 1: every address starts with /v1.

The reference is built from the code that answers your requests, and the API serves the same description at https://api.americaspoliticalpulse.com/v1/openapi.json (no key needed). The terms let us change the limits or the API, with notice where we can.

Back to topNext: Terms and citation

Terms and citation

The data are CC BY 4.0: use them for anything, and cite them.

Using the API means accepting the terms of use: one account per person, keep your key to yourself, and stay within the limits. Every response carries the citation in meta.citation:

Westwood, S. J., & Lelkes, Y. (2024). America's Political Pulse [Data set]. https://americaspoliticalpulse.com/data

When you use a measure, cite its methods paper too. meta.sources links to the outside sources behind the data, their terms and the credit each asks for. One exception: caption text from broadcasters and video platforms in the state floor speeches file, and campaign-website text in the websites file, remain under their sources’ terms, not CC BY 4.0.

Reference
Every endpoint, parameter and field: the API reference. The full description is the OpenAPI file.

Back to top