Article type: How-to guide (one task: pull federal job postings from the USAJOBS Search API into a CSV, and know what it costs before you schedule it)

Prerequisites: Python 3.9+, requests, a free USAJOBS API key from developer.usajobs.gov and the email you registered it with. An Apify account only if you use the hosted option at the end.

Tested on: 14 November 2026, against the live API.

"How much will it cost to pull federal jobs every week?" has two answers. If you call the official USAJOBS Search API yourself, the data costs nothing: the key is free, and on 14 November 2026 our script pulled every public job posted in the last 7 days (3,479 of them) in 7 requests and 12 seconds. If you use a hosted, pay-per-row tool instead, you pay per job, so it's worth doing the sums before you schedule anything.

This guide does both: it sizes the pull, writes the CSV, and works out the cost per 1,000 rows, per week and per alert, with real counts from that day.

Step 1: Put your key in environment variables

export USAJOBS_KEY="your-key" export USAJOBS_EMAIL="the-email-you-registered@example.com" 

USAJOBS wants the email as the User-Agent header and the key as Authorization-Key. Keeping both in the environment keeps them out of your code and your Git history.

Step 2: Size the pull before you run it

Cost and run time both depend on the number of rows, so count first. Ask for one result per page and read SearchResultCountAll. Save as size.py:

import os, time, requests HEADERS = {"Host": "data.usajobs.gov", "User-Agent": os.environ["USAJOBS_EMAIL"], "Authorization-Key": os.environ["USAJOBS_KEY"]} def how_many(**params): r = requests.get("https://data.usajobs.gov/api/Search", headers=HEADERS, timeout=60, params={**params, "ResultsPerPage": 1}) # one row is enough to read the count r.raise_for_status() time.sleep(1) return int(r.json()["SearchResult"]["SearchResultCountAll"]) for label, params in [ ("All open jobs", {}), ("Public, posted in the last 7 days", {"WhoMayApply": "Public", "DatePosted": 7}), ("Public, posted in the last 30 days", {"WhoMayApply": "Public", "DatePosted": 30}), ("Public, series 2210 (IT), open now", {"WhoMayApply": "Public", "JobCategoryCode": "2210"}), ("Public, series 2210, posted in the last 30 days", {"WhoMayApply": "Public", "JobCategoryCode": "2210", "DatePosted": 30}), ]: n = how_many(**params) print(f"{label:<50} {n:>6} ~{-(-n // 500)} requests") 

Output on 14 November 2026:

All open jobs 10000 ~20 requests Public, posted in the last 7 days 3479 ~7 requests Public, posted in the last 30 days 7636 ~16 requests Public, series 2210 (IT), open now 275 ~1 requests Public, series 2210, posted in the last 30 days 208 ~1 requests 

Two things to notice. First, "All open jobs" says exactly 10,000. That's the most one query will return, so the count is capped and the real total is unknown. Split a broad pull by posting date, agency or series until each part is under 10,000. Second, the API returns up to 500 jobs a page, so requests = rows ÷ 500, rounded up. Even a big weekly pull is a handful of calls.

Step 3: Write the CSV

The response is nested JSON. This script flattens the fields most people put in a spreadsheet, keys rows by job ID so a job can't appear twice, and refuses to run a search that has hit the 10,000 cap. Save as usajobs_to_csv.py:

import csv, os, sys, time, requests HEADERS = { "Host": "data.usajobs.gov", "User-Agent": os.environ["USAJOBS_EMAIL"], "Authorization-Key": os.environ["USAJOBS_KEY"], } PARAMS = {"WhoMayApply": "Public", "DatePosted": 7} # your search goes here PER_PAGE = 500 # the most the API returns per page def flatten(item): j = item["MatchedObjectDescriptor"] pay = (j.get("PositionRemuneration") or [{}])[0] d = j.get("UserArea", {}).get("Details", {}) return { "id": item["MatchedObjectId"], "title": j["PositionTitle"], "agency": j["OrganizationName"], "series": ";".join(c["Code"] for c in j.get("JobCategory", [])), "grade": f'{(j.get("JobGrade") or [{}])[0].get("Code", "")}-{d.get("LowGrade", "")}/{d.get("HighGrade", "")}', "pay_min": pay.get("MinimumRange"), "pay_max": pay.get("MaximumRange"), "pay_interval": pay.get("RateIntervalCode"), "where": j["PositionLocationDisplay"], "posted": j["PublicationStartDate"][:10], "closes": j["ApplicationCloseDate"][:10], "url": j["PositionURI"], } rows, page, requests_made, started = {}, 1, 0, time.time() while True: r = requests.get("https://data.usajobs.gov/api/Search", headers=HEADERS, timeout=60, params={**PARAMS, "ResultsPerPage": PER_PAGE, "Page": page}) r.raise_for_status() requests_made += 1 result = r.json()["SearchResult"] if page == 1: total = int(result["SearchResultCountAll"]) if total >= 10000: sys.exit("10,000 or more matches: narrow the search, or you'll silently miss jobs") for item in result["SearchResultItems"]: rows[item["MatchedObjectId"]] = flatten(item) # keyed by ID, so no duplicates if result["SearchResultCount"] < PER_PAGE: break page += 1 time.sleep(1) # be polite with open("federal_jobs.csv", "w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(next(iter(rows.values())))) w.writeheader() w.writerows(rows.values()) print(f"{len(rows)} jobs ({total} matched) in {requests_made} requests, {time.time() - started:.0f} s") 
python usajobs_to_csv.py 

Output on 14 November 2026:

3479 jobs (3479 matched) in 7 requests, 12 s 

The first two rows of federal_jobs.csv:

id,title,agency,series,grade,pay_min,pay_max,pay_interval,where,posted,closes,url 847023900,AIRCRAFT ENGINE MECHANIC (Title 32),Air National Guard Units,8602,WG-10/10,31.42,36.64,PH,"Kelly AFB, Texas",2026-11-13,2026-12-13,https://www.usajobs.gov:443/job/847023900 850096500,Transportation Security Officer,Transportation Security Administration,1802,SV-D/D,65273,80476,PA,"Edgartown, Massachusetts",2026-11-12,2026-12-10,https://www.usajobs.gov:443/job/850096500 

The file was 680,575 bytes, about 196 bytes a row, so 1,000 jobs come to roughly 0.2 MB. Storage isn't the cost here.

To change the search, edit PARAMS. For example, add "JobCategoryCode": "2210" for IT jobs or "LocationName": "Denver, Colorado" for one city.

Step 4: Work out the cost

Doing it yourself: $0 for the data. You pay in your own time and a place to run it: a cron job, a server or a scheduled CI job. The numbers above are the whole load: 7 requests for a week of public postings.

A hosted pay-per-row tool: you pay per job returned. We use our own Actor's live price list as the example (more on it below): $0.001 per job at the base price, $0.00005 Apify start fee per run, and $0.01 for a monitor run that finds new jobs. Here are the sums with that day's counts. We didn't run the Actor for this post; these figures are arithmetic from the price list.

Pull Rows Sum Cost
1,000 jobs, once 1,000 1,000 × $0.001 + $0.00005 $1.00005
Every public job posted in the last 7 days 3,479 3,479 × $0.001 + $0.00005 $3.47905
One query at the 10,000 cap 10,000 10,000 × $0.001 + $0.00005 $10.00005
IT (series 2210) jobs posted in the last 30 days 208 208 × $0.001 + $0.00005 $0.20805

Per 1,000 rows that's $1.00 at the base price ($0.90 on Apify's Silver plan, $0.80 on Gold and above), plus the start fee.

The schedule matters more than the price per row. Say you want a running feed of public IT jobs. The 208 open series 2210 jobs posted between 19 October and 14 November were posted on 22 different days. Three ways to follow them over those 27 days:

Approach Sum Cost over 27 days
Pull all open 2210 jobs every day (275 on 14 Nov) 27 × (275 × $0.001 + $0.00005) about $7.43
Daily monitor run (new jobs only) 208 × $0.001 + 22 runs with new jobs × $0.01 + 27 × $0.00005 about $0.43
Weekly monitor run 208 × $0.001 + 4 × $0.01 + 4 × $0.00005 about $0.25

Re-pulling the same open jobs every day is what makes a feed expensive. Return only new jobs, and the $0.01 alert fee becomes the biggest part of a daily run, so a weekly schedule halves it again. One caveat: these counts are the jobs still open on 14 November. Jobs posted and already closed in that window aren't in the search, so the real figures would be a little higher.

At the other end, a weekly CSV of every new public job (3,479 that week) would be about $3.48 a week through a hosted tool, or around $181 a year at that volume. For one broad pull you already run on your own server, the script above is the cheaper choice.

Things that will catch you out

  • Pay isn't one unit. In this pull, 2,803 rows were per year (PA), 663 per hour (PH), 11 without compensation (WC) and 2 used FB. Sorting on pay_min mixes hourly and yearly figures. Convert before you compare.
  • Postings bunch on weekdays. Monday 9 November had 779 new public jobs, Tuesday 945, Wednesday 90, Thursday 698 and Friday 899. The weekend before had 32 and 4. A daily schedule's cost swings by day, and weekend runs mostly find nothing.
  • "Multiple Locations". 329 rows say just that, and 28 say "Location Negotiable After Selection". The individual locations are in the PositionLocation list if you need them.
  • One job, several series. 64 rows listed more than one occupational series. The script joins them with ;, so filter with "contains", not "equals".
  • The 10,000 cap is silent. The API doesn't raise an error, it just stops. Keep the check in Step 3.

The hosted option (Disclosure: we built this)

If you'd rather not run the script, our Apify Actor USAJOBS API (Unofficial) wraps the same official Search API. You don't need a USAJOBS key, and you can export the results as CSV, JSON or Excel. It's a USAJOBS data tool, not an OPM or USAJOBS product. Monitor mode returns only jobs it hasn't returned before. If you turn off "return current jobs on the first monitor run", the first run saves what's open now without charging for any jobs. A search that hits the 10,000 limit gets a free scan_limit status row instead of stopping silently.

{ "jobCategoryCodes": ["2210"], "monitorMode": true, "emitBaselineOnFirstRun": false, "maxResults": 0 } 

Pricing is pay per event: $0.001 per job returned (price from 14 November 2026; higher Apify plans pay less), $0.01 per monitor run that returns at least one new job, and Apify's $0.00005 Actor start per run. Status rows, jobs your filters remove and jobs already reported are free.

USAJOBS API (Unofficial) on Apify

Not an official USAJOBS or OPM product, and not endorsed by any federal agency. Always check the official announcement on usajobs.gov before you apply.

Your Evenings