Public Health API Practice Sandbox

Phase 1 — no login required Phase 2 — API key required

This is a practice API built for the "Building Practical API Workflows for Public Health" workshops. It behaves like a real public-health data API — you'll ask it questions and it will hand back data — but everything here is safe to experiment on and won't break anything.

The one thing you need to know: the base URL

https://test-api.seminars.page

Everything you do starts by adding something to the end of this address.

What data is available

There are two "resources" — think of each as a table you can ask questions of:

ResourceWhat it containsAddress
CasesWeekly reported counts of a few notifiable conditions, by state and county/v1/cases
ImmunizationsMonthly doses administered for a few vaccines, by state and county/v1/immunizations

Getting a key: the credential step (Advanced workshop)

Both resources now require you to prove you're allowed to ask for data. This happens in two steps, and it mirrors how most real public-health APIs work:

  1. You were given a classroom API key — a shared password for this workshop, distributed by your facilitator. Think of it like a house key: it works, but if it's lost or shared too widely, everyone in the house has to get new keys.
  2. You exchange that key for a temporary token — think of this like a bus ticket: it only works for a little while, and if you lose it, only that one ticket needs replacing, not the whole house's locks.

Step 1 — exchange your classroom key for a token

POST https://test-api.seminars.page/v1/token
Header: X-API-Key: YOUR-CLASSROOM-KEY

Response:

{
  "token": "3F82ac9K...",
  "token_type": "Bearer",
  "expires_in": 900
}

expires_in is in seconds — this token stops working after 15 minutes, and you'll need to repeat this step to get a new one. That's deliberate: it's the whole point of the "temporary credential" concept.

Step 2 — use the token on every request

Add it as a header on /v1/cases and /v1/immunizations requests:

GET https://test-api.seminars.page/v1/cases?state=CA&limit=25
Header: Authorization: Bearer 3F82ac9K...

Doing this in Excel Power Query

  1. Data → Get Data → From Other Sources → From Web
  2. Choose Advanced (not Basic) so you can add headers
  3. For the token step: set the URL to https://test-api.seminars.page/v1/token, and under "HTTP request header parameters" add X-API-Key with your classroom key. You'll need to switch the request to POST — Power Query's "From Web" dialog exposes this under its advanced options.
  4. Copy the token value out of the response
  5. For each data request: set the URL to /v1/cases or /v1/immunizations as before, and add a header named Authorization with the value Bearer followed by your token (note the space after "Bearer")

Example request — Cases

https://test-api.seminars.page/v1/cases?state=CA&limit=25

This asks for up to 25 case records from California. Here's what comes back:

{
  "data": [
    {
      "id": 1,
      "state": "CA",
      "county": "Los Angeles",
      "report_date": "2026-06-01",
      "case_type": "influenza-like illness",
      "case_count": 12
    }
  ],
  "next_cursor": "MjU=",
  "count": 25
}

Parameters you can use

ParameterRequired?What it does
stateNoTwo-letter state code, e.g. CA, TX, NY, FL, WA. Leave it out to get every state.
limitNoHow many rows per page, from 1–50. Defaults to 25. The API will never hand back more than 50 rows in one response — see "Pagination" below.
cursorNoWhere to pick up from. You get this value from the previous response's next_cursor — you never make it up yourself.

Example request — Immunizations

https://test-api.seminars.page/v1/immunizations?county=King&limit=25
ParameterRequired?What it does
countyNoCounty name, e.g. King, Harris, Erie. Leave it out to get every county.
limitNoSame as above — 1 to 50, default 25.
cursorNoSame as above.

Pagination — why you sometimes need a "second page"

This API deliberately never hands you an entire dataset in one response, even if the whole thing would technically fit. Real public-health APIs work the same way, both to protect their servers and because most tools (Excel included) work better with data in manageable chunks.

Every response includes a next_cursor value. If it's a piece of text, there's more data waiting — take that value and add it to your address as &cursor=THAT_VALUE to get the next page. If next_cursor is null, you've reached the end.

https://test-api.seminars.page/v1/cases?state=CA&limit=25&cursor=MjU=

Rate limiting — why you might get slowed down

Each token can make up to 20 requests per minute. If you go over that, you'll get an error back instead of data, with a hint about how long to wait:

HTTP 429 Too Many Requests
Retry-After: 37

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Try again in 37 seconds."
  }
}

This is normal, and it's there on purpose — real APIs do this to stay reliable for everyone using them. Retry-After tells you how many seconds to wait before trying again.

A data-quality note worth watching for

Missing is not the same as zero. Somewhere in this dataset, a case count or dose count is genuinely missing — the field shows up as blank/null, meaning nobody recorded a value. Elsewhere, a count is legitimately reported as 0 — meaning zero cases or zero doses were actually observed. In Excel, both can render as an empty-looking cell if you're not careful, but they mean very different things for analysis. Part of this exercise is learning to tell the two apart before you calculate an average or a total.

Errors you might see

StatusMeaning
400Something about your request doesn't make sense to the API — most often an invalid cursor. Try the request again without a cursor to start over.
401Missing or invalid credentials. For /v1/token, check your X-API-Key header. For /v1/cases or /v1/immunizations, your token may be missing, mistyped, or expired — get a new one from /v1/token.
404The address doesn't match a resource this API knows about. Double check /v1/cases or /v1/immunizations.
429You've hit the rate limit — see above. Wait the number of seconds in Retry-After and try again.