Offendersearch
Tutorial

Using a background check API in Node.js.

A working tutorial for calling a background check API from Node.js with the built-in fetch: a small client, TypeScript types for the response, criminal records, batch, an Express webhook handler and retries.

25 free calls · No card · $0.15 a call, $0.11 past 2,000 a month

const res = await fetch(
  'https://api.offendersearch.app/v1/search',
  {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.OFFENDERSEARCH_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: {
        firstName: 'John',
        lastName: 'Doe',
        dob: '1980-04-12',
      },
    }),
  },
);
const { records, sourceStatus } = await res.json();
Before you start

What you need to call a background check API from Node.js

Node 18 or newer, which ships fetch, and an API key in the OFFENDERSEARCH_KEY environment variable. Sign up to create one — it works at once on 25 free calls, no card. There is no package to install.

One rule before any code: the key belongs on your server. Never call the API from browser JavaScript, where the key would be visible to every visitor. Call your own backend route, and let that route call the API.

Step 1

A small fetch client and your first search

Every endpoint takes the same X-API-Key header and returns JSON, so one helper covers the API. It sets a timeout with AbortSignal.timeout and turns a non-2xx response into an error carrying the API’s own error.code and error.message.

// client.mjs — Node 18+ has fetch built in, so there is nothing to install.
const BASE = 'https://api.offendersearch.app';

export async function api(path, body, { method = 'POST', headers = {} } = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: {
      'X-API-Key': process.env.OFFENDERSEARCH_KEY,
      'Content-Type': 'application/json',
      ...headers,
    },
    body: body ? JSON.stringify(body) : undefined,
    signal: AbortSignal.timeout(60_000),
  });
  const json = await res.json();
  if (!res.ok) {
    const err = new Error(json.error?.message ?? res.statusText);
    err.status = res.status;
    err.code = json.error?.code;
    err.retryAfter = Number(res.headers.get('Retry-After')) || undefined;
    throw err;
  }
  return json;
}

const data = await api('/v1/search', {
  query: { firstName: 'John', lastName: 'Doe', dob: '1980-04-12' },
});
console.log(data.records.length, 'records');
Step 2

Type the response and triage the matches

Two fields drive every decision: matchState on each record and sourceStatus on the response. Typing them keeps a reviewer queue honest — only a full date-of-birth match is treated as an identification.

type MatchState = 'dob_match' | 'year_match' | 'age_match' | string;

interface SearchRecord {
  matchState: MatchState;
  matchConfidence: number;
  matchBasis: string;
  sources: { code: string; level: string }[];
}

interface SearchResponse {
  records: SearchRecord[];
  sourceStatus: { source: string; ok: boolean; incomplete: boolean }[];
}

export function triage(res: SearchResponse) {
  const confirmed = res.records.filter((r) => r.matchState === 'dob_match');
  const review = res.records.filter((r) => r.matchState !== 'dob_match');
  const unanswered = res.sourceStatus.filter((s) => !s.ok).map((s) => s.source);
  return { confirmed, review, unanswered };
}

All match states are defined in the matching reference; all record fields in the record object.

Step 3

Search criminal records from Node.js

Criminal records are at POST /v1/criminal/search on the same key. A last name is required, results page with page and perPage, and each record states its type — booking, custody, court case or warrant. See the criminal record reference.

const res = await fetch(
  'https://api.offendersearch.app/v1/criminal/search',
  {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.OFFENDERSEARCH_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: {
        firstName: 'Morgan',
        lastName: 'Okafor',
        dob: '1990-06-18',
        state: 'OK',
      },
      page: 1,
      perPage: 50,
    }),
  },
);
const { status, records, sourceStatus } = await res.json();
Step 4

Batch up to 1,000 people in one request

POST /v1/batch takes a queries array of up to 1,000 people. Results come back with the index of the row they answer. Prefer one batch to a loop of single calls: it is one round trip and is billed the same, per row.

const people = [
  { firstName: 'John', lastName: 'Smith', state: 'FL', dob: '1980-04-12' },
  { firstName: 'Jane', lastName: 'Doe', state: 'TX' },
];

const { results } = await api('/v1/batch', { queries: people });

for (const r of results) {
  const person = people[r.index];            // join back by index
  console.log(person.lastName, r.status, r.counts.records);
}
Get an API key25 free calls, no card.
Step 5

Async searches and an Express webhook handler

POST /v1/searches accepts a search and returns a searchId; a webhookUrl gets a notification when it finishes. Acknowledge the webhook straight away, then fetch the search with your own key — that response is the source of truth.

import express from 'express';
import { randomUUID } from 'node:crypto';
import { api } from './client.mjs';

// 1. Start the search. Reuse the same Idempotency-Key if you retry this request.
const job = await api(
  '/v1/searches',
  {
    query: { lastName: 'Doe', firstName: 'John', dob: '1980-04-12' },
    webhookUrl: 'https://example.com/hooks/offendersearch',
  },
  { headers: { 'Idempotency-Key': randomUUID() } },
);

// 2. Receive the notification. It is unsigned and sent once, so fetch the
//    search with YOUR key and trust that response instead of the payload.
const app = express();
app.post('/hooks/offendersearch', express.json(), async (req, res) => {
  res.sendStatus(200);
  const result = await api(`/v1/searches/${req.body.searchId}`, null, { method: 'GET' });
  await saveResult(result.searchId, result);   // de-duplicate on searchId
});
Step 6

Retries, rate limits and error codes

Retry on 429 and on 5xx only. A 429 carries a Retry-After header in seconds; honour it rather than guessing.

export async function withRetry(fn, attempts = 4) {
  for (let i = 0; ; i++) {
    try {
      return await fn();
    } catch (err) {
      const retryable = err.status === 429 || err.status >= 500;
      if (!retryable || i === attempts - 1) throw err;
      const wait = err.retryAfter ? err.retryAfter * 1000 : 2 ** i * 1000;
      await new Promise((r) => setTimeout(r, wait));
    }
  }
}

const data = await withRetry(() =>
  api('/v1/criminal/search', { query: { lastName: 'Okafor', state: 'OK' } }),
);
StatusRetry?Meaning
400NoA parameter is missing or invalid
401NoMissing or invalid X-API-Key
402NoA payment method is needed to continue
422NoUnderstood and declined; the message says how to narrow the query
429Yes, after Retry-AfterRate limit or quota reached
5xxYes, with backoffA temporary server-side failure

Full list in the errors reference.

Frameworks

Using it from Next.js, Express or a serverless function

In Next.js, call the API from a route handler or a server action, never from a client component. In Express, wrap it in a route your front end calls. In a serverless function, prefer the async endpoint for anything that might outlast the platform’s request limit, and let the webhook wake a second function.

Standard API results are not a consumer report and must not be used for a hiring or tenant decision; those run as FCRA workflows through FCRA Partner Access.

Related

More on the background check API

The product is on the background check API page. The same walkthrough in Python is background check API in Python, and the language-neutral guide is background check API integration. For AI agents, the MCP server for offender search exposes the same searches as tools.

FAQ

Background check API Node.js questions

Is there an npm package for the background check API?

No package is needed. Node 18+ includes fetch, and the client on this page is about twenty lines.

Can I call the background check API from the browser?

No. That would expose your API key. Call it from your server and return only what the page needs.

Does it work with TypeScript?

Yes. The types on this page cover the fields that drive decisions, and the OpenAPI definition at /openapi.json can generate a full client.

Can I use axios instead of fetch?

Yes. Any HTTP client works with the same URLs, header and JSON bodies.

How do I verify the webhook?

The async-search webhook is unsigned and sent once. Treat it as a notification and fetch GET /v1/searches/{searchId} with your key to get the result.

How many checks can I send at once?

Up to 1,000 people per batch request. Send larger lists as several batches.

Are there free calls for testing from Node.js?

Every new account gets 25 free calls with no card, on the production endpoints.

Does it run on Vercel, Cloudflare Workers or AWS Lambda?

Yes. It is plain HTTPS, so any runtime with fetch can call it. Keep the key in the platform’s secret store.

Run the first search from Node

Create a key, export it, and run client.mjs. 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

Criminal records

Build with it

Screening and compliance