Developer docs · v1.0.0

Background check API vs building your own records pipeline: the build-or-buy decision

What it costs to write and keep your own integrations against county court portals, jail rosters and state registry sites, what one API with one schema gives you instead, what it does not, and how to evaluate both with the same twenty names.

Updated November 3, 2026

Background check API vs building your own records pipeline: the decision in one table

There are two ways to put public criminal and registry records into a product. You can write integrations yourself against the websites that publish them, one parser per county court portal, jail roster, corrections lookup and state registry, and keep those parsers working as each site changes. Or you can call one background check API that answers in one schema and bill it per call. This page is for the engineer or product owner weighing those two, and it keeps to the questions you will actually face: maintenance, identity resolution, what you get, what you do not, cost, and when the build is still right.

QuestionYour own integrationsOne API
Integrations to writeOne per site; a county may have severalOne request per search
SchemaOne per site, normalised by youOne, documented, same keys every time
When a site changes its HTMLYour parser breaks; you notice when results go quietNothing changes for you
Identity across sourcesYours to solve, per sitepersonRef, matchState, unverified on every record
CompletenessUnknown unless you track it per sitestatus, counts, sourceStatus on every response
CitationsStore the URL yourself, per parsersources[].recordUrl on every record
TermsCheck each site's terms and ask counselOne agreement
CostEngineer-weeks to build, weeks a year to keep$0.15 a call, $0.11 past 2,000 calls in a month, 25 free calls, no card

What building your own court website integrations costs: parsers, formats and maintenance

The first parser is the cheap one. A county court portal with a name search and a results table takes an afternoon: submit the form, read the rows, follow the case link, pull the charges. The cost arrives afterwards, and it compounds. Counties move to new case-management vendors and the HTML changes completely. A sheriff's roster adds a consent page. A state corrections lookup starts answering with a JavaScript-rendered table instead of server HTML. A court changes its date format from 03/02/2015 to 2015-03-02 in one column and not the others. None of this is announced; you find out when a parser returns an empty list and nobody is sure whether that means "no record" or "broken".

Multiply that by the number of sites. There are thousands of county courts in the United States, and the jurisdictions you care about will each have a court portal, a jail roster and often a separate corrections lookup, in different vendors' formats. A team that keeps thirty of these working reliably has a standing maintenance job; a team that keeps three hundred working has a product, and it is a different product from the one they set out to build.

  • Format drift. Dates, name order (LAST, FIRST M versus First M Last), charge codes, disposition vocabularies and case-number formats all vary per site and change over time.
  • Silent failure. A broken parser and a clear subject look identical from the outside: both return nothing. Distinguishing them means per-site health checks, which is more code to keep.
  • Access controls. Consent pages, rate limits, CAPTCHAs and login walls appear without notice. Each one is a per-site workaround and each workaround is more to maintain.
  • Detail pages. The results list rarely carries the disposition. Getting it means a second request per hit, with its own parser and its own drift.
  • Normalisation. Thirty sites give you thirty schemas. Folding them into one that your product can read is a project of its own, and the one that never quite finishes.

Terms and exposure when automating court websites

Every site you automate has terms of use, and some are explicit about automated access. Check each site's terms and ask counsel. That is the whole of this section: it is a question for your lawyer, not for a tutorial, and the only engineering point is that the answer is per site, so it is one more thing that scales with the number of integrations you keep.

Identity resolution: why one schema beats a pile of court website pages

The hard problem is not fetching a page; it is knowing whether the *Michael Johnson* on a county docket is your applicant. Sites differ in what birth evidence they publish: a full date, a year, an age, or nothing. A parser that treats a name match as a hit will flag the wrong person on every common name; a parser that only accepts a full-date match will miss a hit on a site that publishes years. Each site needs its own rule, and the rules must agree with each other or the same person is a hit in one county and a miss in the next.

An API that returns one schema has already made these decisions once and applies them identically to every record, so your code reads the same four fields whatever the source:

One schema, four reads
// The same four reads, whichever source a record came from.
for (const rec of data.records) {
  const who   = rec.personRef;                       // one key per person, across sources
  const sure  = rec.matchState === "dob_match";      // the only automatic identification
  const kind  = rec.recordType;                      // court_case | jail_booking | doc_inmate | warrant | …
  const cites = rec.sources.map((s) => s.recordUrl); // where a reviewer confirms it
}
FieldWhat it settles
personRefOne key per person across listings and sources, so the same subject found in two places is one record with two entries in sources[], not two hits to reconcile.
matchStateThe identification verdict: dob_match (full date agrees), year_match, age_match, no_dob_age_year (nothing to check; name only), dob_near_match (only with dobTolerance), or null when you sent no date.
unverifiedOne boolean: false only for dob_match. The field a review queue branches on.
recordTypeWhich schema the record follows: sex_offender on the registry side; court_case, jail_booking, doc_inmate, warrant, wanted_fugitive, registry or healthcare_exclusion on the criminal side.
cases[].charges[].dispositionWhether a charge ended in a conviction, a dismissal or is still open. The field that decides whether a court record counts.

A record whose published date of birth contradicts yours is not returned at all, rather than returned with a low score, which removes a whole class of false positives before your code sees them. The matching reference and the guide on false positives describe the rules; the point here is only that they are one set of rules.

What a background check API gives you in one request

  • Registry breadth. POST /v1/search answers from the 58 US registries in one request, with counts.sourcesQueried and one sourceStatus row per registry. See the sex offender API.
  • Criminal layers. POST /v1/criminal/search returns court cases, jail bookings, corrections records, warrants and exclusions, each sources[] entry carrying a level of county, state or federal. See the criminal records API.
  • Completeness you can read. status is complete or partial; counts.sourcesIncomplete and each sourceStatus row's incomplete and incompleteReason say exactly which part of the answer is a lower bound.
  • A citation on every record. sources[].recordUrl is the page a reviewer opens, with lastCheckedAt beside it.
  • One schema. Every key is always present on a criminal record, with "", null or [] when the source publishes nothing, so a parser can be strict.
  • A continuously updated dataset, plus an optional live block when a decision needs a source re-verified now, at $0.02 per completed source.
  • Per-call pricing. $0.15 a call, $0.11 past 2,000 calls in a month, 25 free calls, no card. Batch rows bill the same way.
  • A sandbox with documented test people and a free os_test_ key, so the evaluation below costs nothing.

What the background check API does not do

A fair comparison names the gaps, and these are the ones that decide the build for some teams:

  • No Social Security number. The query is name, date of birth or age, and place. There is no SSN field and no SSN trace.
  • No credit. Nothing about credit history, employment history or education is returned.
  • No international records. The dataset is United States public records.
  • Not a consumer report. Every criminal response carries legal.notice. A hiring, tenant, credit or insurance decision runs as an order through the FCRA Partner API, which is a separate, approved-partner flow; consult counsel on which applies to you.
  • Not every field on every source page. A record is normalised; if your product needs a field that only one portal publishes and the API does not return, that one portal may still be yours to integrate.

Cost comparison: engineer-weeks vs per-call price

Do not compare a hosting bill with an API bill; the hosting is the small part of the build. The real build cost is engineering time, and it has two components. The first is the initial integrations: for each site, the parser, the detail-page parser, the normalisation into your schema, the health check and the identity rule. The second is the standing maintenance: every site that changes, every new access control, every quiet failure found a week late. Teams that have done this describe the first in engineer-weeks per handful of sites and the second as a fraction of an engineer, permanently, that grows with the number of sites kept alive.

Cost lineYour own integrationsOne API
Initial buildEngineer-weeks per batch of sites, before the first useful resultAn afternoon: one endpoint, one key, the sandbox
OngoingA standing share of an engineer; grows with each siteNone beyond the integration you already keep
Per searchCompute and proxies, small$0.15 a call, $0.11 past 2,000 calls in a month
First resultsAfter the build25 free calls, no card
Completeness and identityBuilt and maintained by youIn the response

The arithmetic is simple once you write it down. Take the engineer-weeks you estimate for the build and the share of an engineer for maintenance, convert them at your loaded rate, and divide by the searches you expect in a year. That is your per-search build cost. If it is below the per-call price and stays below it as the site count grows, build. For most teams searching more than a few jurisdictions, it is not close, and the cost that decides it is the maintenance line, not the build line. The pricing page has the per-call schedule for the volume side of the sum.

When building your own records pipeline is still the right call

Sometimes it is, and this page should say so.

  • One jurisdiction, one site. A product that only ever needs one county's docket has one parser to keep. The maintenance argument does not apply, and the API's breadth is not worth paying for.
  • A one-off research project. If you need a dataset once, for a study or an audit, and will not run it again, the standing maintenance cost is zero and the build is a script.
  • A field the API does not return. When the decision depends on something only one portal publishes, that portal is yours to integrate whatever else you buy.
  • A source outside the API's coverage. The criminal coverage page lists the sources on file; if yours is not there, you know before you start.

The mixed answer is common and sensible: the API for breadth, identity and citations, and one or two of your own integrations for the places it does not reach.

A background check API call you can run against the sandbox

This is the entire integration on the API side, against the public API for testing with a free os_test_ key. It searches the sandbox person Felony Record, who has one court case with a felony burglary charge found guilty and a misdemeanour theft charge dismissed, and prints the four reads from above plus the citation. Python and Node.js SDKs ship with the first registry release; see SDKs. Until then the raw HTTP calls below are the integration.

curl -X POST https://sandbox.offendersearch.app/v1/criminal/search \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": {"firstName": "Felony", "lastName": "Record", "dob": "1975-06-15"}}'

Change the body to {"query": {"firstName": "Exact", "lastName": "Match", "dob": "1980-01-01"}} and the path to /v1/search for the registry side. The sandbox allows 60 calls a minute and 2,000 a day per test key; the full list of test people is on the sandbox reference.

How to evaluate the API against your own court website integrations

One controlled test settles the question for your product better than any table on this page.

  1. Define the scope. Write down which record kinds you need (registry listings, court cases, bookings, custody, warrants), which jurisdictions, and what a decision needs from each record: a disposition, a date, a citation. This list is the specification for either option.
  2. Run the same twenty names through both. Pick twenty subjects you already know the answer for, with dates of birth. Run them through your own integrations and through the API (on the sandbox first with the documented test names, then with 25 free production calls). Keep the raw responses.
  3. Compare identity, not just hit counts. For each hit, note whether you could tell it was the same person: did the source publish a date of birth, did it agree, and would your parser have known? Compare that with matchState and unverified on the API record.
  4. Compare citations. For each API record, open sources[].recordUrl. For each of your own hits, check you stored an equivalent link. A result a reviewer cannot confirm is not a result.
  5. Price it. Count the engineer-weeks your twenty names cost to reach, and the weeks per year to keep those integrations working. Set that against the per-call price at your expected monthly volume.
  6. Decide per jurisdiction. The answer can be mixed: an API for the breadth, and your own integration for one portal the API does not cover or where you need a field it does not return.

Background check API vs building your own: questions developers ask

Is it cheaper to build my own court records integration?

For one site, usually yes. For many, the maintenance line decides it: every site that changes its format is an interruption, and a parser that quietly returns nothing is indistinguishable from a clear subject until someone checks. Price the engineer-weeks and the standing maintenance against $0.15 a call at your volume.

Does the API return the same records I would get from the court website?

It returns a normalised record with a citation to the source page in sources[].recordUrl. Run the twenty-name test above on your own jurisdictions to see the overlap for your scope; the criminal coverage page lists what is on file.

How does the API tell me it is the same person?

matchState on every record: dob_match when the published full date of birth agrees with yours, and weaker states when the source publishes less. A record whose date contradicts yours is dropped. personRef is one key per person across sources.

Can I use the API results for hiring or tenant screening?

Not directly: the standard endpoints are not a consumer report, and the criminal response says so in legal.notice. Those decisions run as orders through the FCRA Partner API. Consult counsel.

What if I need a jurisdiction the API does not cover?

Keep your own integration for that one and use the API for the rest. Nothing about the API stops you mixing; sources[] tells you which records it answered for, so your own hits are easy to merge in.

Is there a free way to try the API before deciding?

Yes. The sandbox answers with documented test people on a free os_test_ key, and the first 25 production calls are free with no card; see the free background check API page.

What this page does not cover

  • How to write the request body in full: fields, match modes, live and include are in the criminal search reference and the background check API tutorial.
  • Reading every key of the response: see the response explained.
  • Legal questions about automating any site, or about using any record in a decision: those are for counsel.
  • Comparing hosted screening vendors: that is the Checkr API alternative page.