Using a background check API in Python.
A working tutorial for calling a background check API from Python with requests: the first search, reading match states, criminal records, batch, async with webhooks, and error handling you can paste into a service.
25 free calls · No card · $0.15 a call, $0.11 past 2,000 a month
import os, requests
r = requests.post(
"https://api.offendersearch.app/v1/search",
headers={"X-API-Key": os.environ["OFFENDERSEARCH_KEY"]},
json={"query": {
"firstName": "John",
"lastName": "Doe",
"dob": "1980-04-12",
}},
timeout=60,
)
records = r.json()["records"]
strong = [
x for x in records if x["matchState"] == "dob_match"
]What you need to call a background check API from Python
Python 3.8 or newer, the requests library, and an API key. Sign up to create one; it works immediately on 25 free calls with no card. There is no SDK to install — the API is plain HTTPS with JSON, so requests is the whole dependency list.
python -m venv .venv && source .venv/bin/activate
pip install requests
export OFFENDERSEARCH_KEY="your_key_here"Make your first background check API call in Python
Authentication is one header, X-API-Key. A requests.Session sets it once and reuses the connection. The search itself is POST /v1/search with a query object; a last name is the minimum, and a full date of birth is what makes the result usable.
import os
import requests
BASE = "https://api.offendersearch.app"
session = requests.Session()
session.headers.update({"X-API-Key": os.environ["OFFENDERSEARCH_KEY"]})
def search_registry(first_name, last_name, dob=None, state=None):
query = {"firstName": first_name, "lastName": last_name}
if dob:
query["dob"] = dob # ISO date: "1980-04-12"
if state:
query["state"] = state
r = session.post(f"{BASE}/v1/search", json={"query": query}, timeout=60)
r.raise_for_status()
return r.json()Read the response: match states and source status
Two fields decide what you do with a response. matchState on each record says how it matched, and sourceStatus says which jurisdictions answered. Filter on the match state, not on the confidence score alone.
def triage(response):
"""Split records into identifications and leads for a human."""
confirmed, review = [], []
for rec in response["records"]:
if rec["matchState"] == "dob_match":
confirmed.append(rec)
else: # year_match, age_match, no date on file …
review.append(rec)
# A clear result only counts if every source answered.
unanswered = [s["source"] for s in response["sourceStatus"] if not s["ok"]]
return confirmed, review, unanswered
data = search_registry("John", "Doe", dob="1980-04-12")
confirmed, review, unanswered = triage(data)
print(len(confirmed), "confirmed,", len(review), "to review,", len(unanswered), "sources unanswered")| matchState | Meaning | Typical handling |
|---|---|---|
dob_match | The record’s full date of birth equals the one you sent | Treat as an identification |
year_match | The record publishes a birth year only, and it matches | Send to a reviewer |
age_match | No date on file; the published age fits your date within a year | Send to a reviewer |
| Anything else | No date or age to check against | Treat as a name-only lead |
Every state is defined in the matching reference.
Search criminal records from Python
Criminal records live at POST /v1/criminal/search on the same key. A last name is required. Results are paginated with page and perPage, and each record says what type it is — a jail booking, a custody record, a court case or a warrant — so your code never treats an arrest as a conviction. Fields are in the criminal record reference.
import os, requests
r = requests.post(
"https://api.offendersearch.app/v1/criminal/search",
headers={"X-API-Key": os.environ["OFFENDERSEARCH_KEY"]},
json={
"query": {
"firstName": "Morgan",
"lastName": "Okafor",
"dob": "1990-06-18",
"state": "OK",
},
"page": 1,
"perPage": 50,
},
timeout=30,
)
for rec in r.json()["records"]:
source = rec["sources"][0]["code"]
print(rec["matchState"], rec["matchConfidence"], source)Run a batch of background checks in one request
POST /v1/batch takes up to 1,000 people per call as a JSON queries array (or a CSV body). Each result carries the index of the row it answers, so you can join results back to your own list without matching on names. A batch is billed per row.
people = [
{"firstName": "John", "lastName": "Smith", "state": "FL", "dob": "1980-04-12"},
{"firstName": "Jane", "lastName": "Doe", "state": "TX"},
]
r = session.post(f"{BASE}/v1/batch", json={"queries": people}, timeout=300)
r.raise_for_status()
for result in r.json()["results"]:
person = people[result["index"]] # zip results back by index
print(person["lastName"], result["status"], result["counts"]["records"])Run a long search asynchronously with a webhook
When nobody is waiting on the answer, POST /v1/searches accepts the search and returns a searchId. Supply a webhookUrl to be notified when it finishes. The notification is sent once and is not signed, so always confirm by fetching GET /v1/searches/{searchId} with your key. An Idempotency-Key header makes a retried request return the original job rather than start a second one.
import time, uuid
job = session.post(
f"{BASE}/v1/searches",
headers={"Idempotency-Key": str(uuid.uuid4())}, # reuse the key when retrying
json={
"query": {"lastName": "Doe", "firstName": "John", "dob": "1980-04-12"},
"webhookUrl": "https://example.com/hooks/offendersearch",
},
timeout=30,
)
job.raise_for_status()
search_id = job.json()["searchId"]
# The webhook is a single unsigned notification, so the GET is the source of truth.
while True:
result = session.get(f"{BASE}/v1/searches/{search_id}", timeout=30).json()
if result.get("status") in ("complete", "partial"):
break
time.sleep(2)Handle errors, rate limits and retries
Every non-2xx response has the same JSON body: a stable error.code to switch on and an error.message you can show a user. One wrapper covers the whole API.
import time
class ApiError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status, self.code = status, code
def post(path, payload, attempts=4):
for attempt in range(attempts):
r = session.post(f"{BASE}{path}", json=payload, timeout=60)
if r.status_code == 429: # rate limited: wait and retry
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
continue
if r.status_code >= 500 and attempt < attempts - 1:
time.sleep(2 ** attempt)
continue
if not r.ok:
err = r.json().get("error", {})
raise ApiError(r.status_code, err.get("code"), err.get("message"))
return r.json()
raise ApiError(429, "rate_limited", "gave up after retries")| Status | What it means | What to do |
|---|---|---|
| 400 | A required parameter is missing or invalid | Fix the request; do not retry |
| 401 | Missing or invalid X-API-Key | Check the key and the header name |
| 402 | A payment method is needed to continue | Add a card in the dashboard |
| 422 | The request was understood and declined, with the reason in the message | Narrow the query as the message says |
| 429 | Rate limit or quota reached | Wait for Retry-After, then retry |
| 504 | A deadline you set was exceeded | Retry, or use the async endpoint |
The full list is in the errors reference.
A short checklist before you ship
- Keep the key in an environment variable or a secret manager, never in the repository.
- Always send a full date of birth when you have one.
- Store the record citations with each result so a reviewer can open the official record.
- Treat an unanswered source as “not clear”, not as “no record”.
- Set timeouts on every request and retry only on 429 and 5xx.
- Do not use standard API results for a hiring or tenant decision — those run as FCRA workflows through FCRA Partner Access.
More on the background check API
The product is described on the background check API page, and the language-neutral build guide is background check API integration. Prefer JavaScript? See the background check API in Node.js tutorial. Costs are on the pay-as-you-go background check API page, and the 25 free calls are explained on the free background check API page.
Background check API Python questions
Is there a Python SDK for the background check API?
There is no SDK to install. The API is JSON over HTTPS, so the requests library is all you need, and the wrapper on this page is about forty lines.
Which Python versions work?
Python 3.8 or newer. The only dependency is requests.
Can I use asyncio or httpx instead of requests?
Yes. The API is plain HTTPS, so httpx or aiohttp work with the same URLs, header and JSON bodies shown here.
How do I run background checks on a pandas DataFrame?
Convert the rows to a list of dicts with firstName, lastName and dob, send them to POST /v1/batch in chunks of up to 1,000, and join the results back using the index field.
How do I avoid false matches on common names?
Send a full date of birth and act automatically only on dob_match. Route year_match, age_match and name-only records to a person.
Is there a free background check API I can use from Python?
Every new account gets 25 free calls with no card, on the same key and endpoints used in this tutorial.
What is the rate limit?
When you exceed it the API returns 429 with a Retry-After header in seconds. Back off for that long and retry, as the wrapper above does.
Can I use this in Django or FastAPI?
Yes. Call it from a view or a background task. For anything that should not block a request, use the async endpoint with a webhook.
Run the first search from your terminal
Create a key, export it, and paste the first example. 25 calls are free.
The Offendersearch APIs
One account, one key, one bill — the sex offender registry and criminal records, side by side.
Sex offender registry
- Sex offender API — One call across 58 US sex offender registries, with scored matches and a citation on every record.
- Sex offender search API — Endpoints, request shapes, record fields, matching and batch, in detail.
- Sex offender database — The national registry data in one schema — photos, aliases, offenses, addresses.
- Registered offender database — What a registered offender record holds, and how its accuracy is measured.
- Bulk sex offender search — Screen a whole list of names in one upload or one API call.
- Sex offender registry API — Choose which of the 58 registries a search covers, and see which ones answered.
- Offender search API — The registry search and the criminal search side by side, on one key.
Criminal records
- Criminal records API — County, state and federal records — jail, prison, court and warrant — through one API.
- Criminal background check API — One cited answer per person for onboarding and screening flows.
- Criminal search API — The search endpoint itself: parameters, record types and coverage, call by call.
- Court records API — Criminal court cases with charges and a disposition on each charge.
- Arrest records API — County jail bookings: booking date, custody status, facility and photo link.
- Inmate search API — State prison and county jail custody, with sentence, release and parole dates.
- Background check API — The registry and criminal layers together, on one key and one bill.
- Employment background check API — The registry and criminal data layer for HR, ATS and screening platforms.
- Employment screening API — The stages of a hiring pipeline, and where criminal and registry checks sit in it.
- Bulk criminal record search — Run a list of names against criminal records in one call.
- Statewide criminal search — One state at a time: prison, court and warrant systems that cover the whole state.
- Federal criminal records — Federal criminal records through the same API, and what a federal search covers.
Build with it
- API documentation — Quickstart, every endpoint, the record object and the OpenAPI spec.
- Sex offender map API — Clusters, pins and names for your own map, and the full record when one is opened.
- Background check API pricing — $0.15 a call, $0.11 past 2,000 a month, 25 free calls to start — worked out by volume.
- Background check API integration — Keys, the first call, async and webhooks, batch, monitoring and errors, step by step.
- Free background check API — 25 free calls on real data, no card — what is included and what comes after.
- Pay-as-you-go background check API — Per call, per report or subscription: the pricing models, with costs by volume.
- Background check API in Node.js — A fetch client, response types, an Express webhook handler and retries.
- NSOPW API — Is there one? What exists, and a developer API for searching US registries.
- MCP server for offender search — Give Claude and other AI agents the same search as a tool.
- Integrations — Screening inside referral software — CarePort, ExtendedCare, Ensocare and Aidin.
- AI integration — Offender safety checks for AI agents and chatbots, with cited answers.
- Offendersearch vs offenders.io — A side-by-side comparison of the two registry APIs.
Screening and compliance
- Tenant background check — Screening applicants for a rental, and where an FCRA workflow is required.
- FCRA Partner Access — The FCRA workflow for hiring and tenant decisions — pricing by quote.
- FCRA-compliant background check data — Accuracy, disputes and permissible purpose for consumer reporting agencies.
- FCRA disputes API — Open, track and resolve a consumer dispute against a record.
- Sex offender monitoring — Watch a person or an address and get an alert when the registry changes.
- Tenant screening API — The registry and criminal layer for property software, with FCRA workflows for rental decisions.
- Non-FCRA background check API — What a public-records lookup may be used for, and when a decision needs FCRA instead.