A public API for testing, with realistic records instead of placeholder data.
The OffenderSearch sandbox at sandbox.offendersearch.app answers with the production serializer’s own output for 51 documented test scenarios: scored matches, per-source status, photographs, charges and dispositions, signed webhooks. Mint a os_test_… key in the dashboard and nothing you send is billed.
curl -X POST "https://sandbox.offendersearch.app/v1/search" \
-H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": {
"firstName": "Exact",
"lastName": "Match",
"dob": "1980-01-01"
}
}'Dummy API, fake API, or a realistic sandbox: which public API for testing you want
A dummy API returns the same hardcoded JSON for a route; a mock API adds a little logic; both exist so a front end has something to render before a back end does, and the well-known ones (placeholder posts, to-dos, products, random users) do that job well. None of them helps when the thing you are building is the integration: a screening flow that has to read a match state, a per-source status, a disposition, or a webhook signature, and has to do it correctly before a real person is on the other end.
The sandbox is the third kind. It is the real API’s code answering from a fixed set of test people, so every response has the production shape and every edge case a screening integration must handle has a name you can send: an exact match with a verified date of birth, a name-only match, an alias, a person on two registries, a clear search, a felony with a dismissed charge beside the conviction, an active warrant, someone in custody.
Dummy JSON API
Fixed responses for fixed routes. Good for rendering a list; says nothing about whether your code read the response correctly.
Mock server
You write the responses. Good for your own contract; you also wrote the bugs into it.
This sandbox
The production serializer, documented people, every scenario a screening flow meets, signed webhooks, free. A test that passes here passes against the live host.
An example API for testing: one request, the exact response
Send the test name below with your test key and this is the answer, captured from the sandbox code. Keys change nothing but the host.
{
"searchId": "srch_sbx_so_exact",
"status": "complete",
"elapsedMs": 236,
"counts": {
"records": 1,
"sourcesQueried": 58,
"sourcesIncomplete": 0,
"sourcesComplete": 58,
"sourcesSkippedByScope": 0,
"recordsReturned": 1
},
"capped": false,
"cappedReason": null,
"cappedLimit": null,
"cappedOmittedSources": [],
"omittedFields": [],
"page": 1,
"perPage": 1,
"totalPages": 1,
"proof": {
"status": "none"
},
"warnings": [],Only the 56 identical “complete, matched 0” registry rows and empty fields are folded, and the fold says so. Read status and counts first, then matchState, then the citation in sources[]. The RESTful API example guide walks this exact response line by line.
The test API URL, the test key and the limits
| Value | |
|---|---|
| Base URL | https://sandbox.offendersearch.app |
| Key | An os_test_… key, in X-API-Key exactly like a live key; minted in the dashboard, up to 3 active per account |
| Rate | 60 requests a minute and 2,000 a day per key; 120 a minute per IP |
| Billing | None. Nothing is recorded to your usage. |
| Shape | Production responses from the production serializers, plus "sandbox": true |
| Errors | The production validators: the same 422s, 401s and 429s your code will meet live |
| Webhooks | Delivered and signed exactly as production signs them, once per scenario |
- 01Create an account, then mint a test key on the keys page of the dashboard. No card.
- 02Point your client at the sandbox host, or let the SDK route an
os_test_key there automatically:pip install offendersearch·npm i @offendersearch/sdk. - 03Send a documented name from the tables below. Any other name returns a complete answer with zero records.
- 04Swap the key for a live one when you ship. Nothing else changes.
JSON test API scenarios for the sex offender search
Names are matched on first and last name together, case-insensitively. A date of birth must be exactly as listed; a different one is a contradiction and returns no match, as in production.
| Scenario | Send | You get back |
|---|---|---|
| Clear visitor · clear | firstName: Clear · lastName: Visitor | No match: a complete answer with zero records. |
| Exact match, date of birth verified · match | firstName: Exact · lastName: Match · dob: 1980-01-01 | One strong match: DOB-verified (matchState dob_match), high confidence, photo, offense, registry state and two addresses. |
| Exact match, no date of birth sent · review | firstName: Exact · lastName: Match | The same person, matched on name alone: matchState is null because no date of birth was sent to verify. |
| Exact match name, different date of birth · clear | firstName: Exact · lastName: Match · dob: 1990-05-05 | No match: the registry's date of birth contradicts the one sent. |
| Name only — possible matches · review | firstName: Name · lastName: Only | Three possible matches in three states. None of the registries publishes a date of birth, so the name is the only evidence (medium confidence). Sending a dob returns the same three with matchState no_dob_age_year and unverified true. |
| Matched through an alias · review | firstName: Alias · lastName: Match | One match whose legal name is different (Jordan Sample Placeholder): matchedName.type is alias, matchBasis carries alias:exact. |
| Registered in another state · review | firstName: Out · lastName: State · state: TX | One match living in Texas but registered by Oklahoma's registry: registrationState OK, addressStates [TX]. |
| One person on two registries · review | firstName: Multi · lastName: State | One record merged from two registries (Florida and Georgia): sources[] lists both. |
| Homeless registrant · review | firstName: Homeless · lastName: Registrant | One match with housingStatus homeless. |
Test scenarios for the criminal records search
A court case with a conviction and a dismissed charge, an arrest with no disposition, a warrant, a person in custody, possible matches, and the healthcare exclusion cases.
| Scenario | Send | You get back |
|---|---|---|
| Clear record | firstName: Clear · lastName: Record | No match: a complete answer with zero records. |
| Felony conviction | firstName: Felony · lastName: Record · dob: 1975-06-15 | One court case: a felony burglary conviction with case number, court, charges, plea, disposition and sentence. DOB-verified. |
| Felony conviction, no date of birth sent | firstName: Felony · lastName: Record | The same court case, matched on name alone: matchState no_dob_age_year and unverified true, because no date of birth was sent to check. |
| Felony record name, different date of birth | firstName: Felony · lastName: Record · dob: 1990-01-01 | No match: the record's date of birth contradicts the one sent. |
| Misdemeanor conviction | firstName: Misdemeanor · lastName: Record · dob: 1988-11-02 | One court case: a misdemeanor theft conviction with a fine. |
| Arrest only | firstName: Arrest · lastName: Only · dob: 1992-04-20 | One jail booking with a charge at booking and no disposition. A booking is not a conviction. |
| Active warrant | firstName: Active · lastName: Warrant · dob: 1985-09-09 | One open warrant (currentStatus active_warrant). |
| Currently in custody | firstName: In · lastName: Custody · dob: 1979-12-31 | One state prison record: currentlyIncarcerated true, custodyFacility, projected release date. |
| Possible matches, name only | firstName: Possible · lastName: Match | Three records with the same name in three states and no date of birth on any of them: possibles to review, not a confirmed identity. |
| Excluded provider, conviction-based | firstName: Excluded · lastName: Provider · dob: 1968-03-22 | One exclusion-list record (recordType healthcare_exclusion, currentStatus excluded): a Medicare/Medicaid exclusion under authority 1128(a)(1), a criminal conviction, so it is returned by default. The exclusion block carries the authority, convictionBased true and the exclusion date. DOB-verified. |
| Administrative exclusion, default search | firstName: Sanctioned · lastName: Provider | No records: this person's only entry is an administrative exclusion (a license suspension, not a conviction), which a search returns only when it sends include: ["exclusions"]. |
| Administrative exclusion, include exclusions | firstName: Sanctioned · lastName: Provider | With include: ["exclusions"], the administrative exclusion comes back: convictionBased false, no date of birth on the list, so matchState no_dob_age_year and unverified true. counts.exclusionsIncluded is true. |
| Clear exclusion check | firstName: Clear · lastName: Provider · dob: 1980-01-01 | No match on any exclusion list, conviction-based or administrative: a complete answer with zero records. |
Test scenarios for webhooks, monitors, the map API and FCRA orders
| Scenario | Send | You get back |
|---|---|---|
| Person monitor, date of birth verified | | 201 Created. One `new` alert at once (matchLabel dob_match, jurisdiction OH), POSTed to the webhook once. |
| Person monitor, name only | | 201 Created. Three `new` alerts (matchLabel name_match), one per sample registrant named Name Only. |
| Person monitor, one person on two registries | | 201 Created. Two `new` alerts — one per registry that holds the person (FL and GA), as production files them. |
| Person monitor, nobody matches | | 201 Created. No alerts. |
| Person monitor, different date of birth | | 201 Created. No alerts: the date of birth contradicts the record. |
| Location monitor around the sample point | | 201 Created. One `new` alert (matchLabel in_radius) for the sample registrant Exact Match, POSTed to the webhook once. |
| Location monitor anywhere else | | 201 Created. No alerts. |
| Criminal person monitor | | 201 Created. One `new` alert (matchLabel dob_match, externalId SBX-CR-2015-000123). |
| Criminal location monitor | | 200 with status not_yet_available, exactly as production: nothing is created. |
| Send a test event (sandbox only) | | Files one more `new` alert on the monitor (its own first match, else the sample registrant) and POSTs it to the monitor's webhook now. Returns the alert and webhookDelivered. |
| A city view: clusters | | Zoom 11 over Columbus, OH: mode clusters, 529 people in 63 clusters. |
| One mile around a point: named pins | | Zoom 15: mode markers, exactly 37 people within the mile (withinRadius 37), 15 named pins plus one approximate-area pin, each with distanceMiles. |
| Many people at one address | | One street-precision pin holding 14 people (a groupId, no inline members); GET /v1/map/groups/{groupId} pages through the 14, all housingStatus homeless. |
| An approximate-area pin | | No street pins; one area pin (precision zip, label 43215) for 9 people whose registry gives only a ZIP code. |
| A drawn polygon | | shape polygon: 22 people inside, 9 pins (the 14-person address is one of them). |
| Open the record behind a pin | | The complete record (a 'Sample …' person, Sampleville, ZIP 00000), a recordRef that opens it again later, and billing {billed: false, reason: sandbox}. |
| A walking route with a stop | | 2.118 miles, 4 route units: 2 people within the 0.1-mile corridor, 1 within 1,000 feet of stop s1, and 9 more placed only to a ZIP (approximate.count 9). |
| Two routes in one call | | shape routes, 8 units, uniquePeople 2 (the same two people on both routes). |
| Plan and usage | | enabled true, sandbox true, the published rates, and zero usage this month. |
| Turning it on or off | | The sandbox map is always on: enable and disable both answer the status with changed false, and nothing is charged. |
| Clear (any other last name) | | Order complete; every search complete with result clear and no records. Any last name not listed here behaves the same way. |
| Records found | | Order complete; each search furnishes one sandbox record (records_found). The sex offender record carries registry{}, the criminal record cases[] with a misdemeanor conviction. |
| Records withheld | | Search complete with result clear: nothing furnished, only withheld counts (2 insufficient_identifiers, 1 not_current_at_source). |
| Sources unavailable | | The search ends unavailable with result incomplete (the sources could not be reached), and the order completes. This is production's documented sandbox outcome for an FCRA order, not a simulated outage of the sandbox. |
| Dispute that resolves | | One furnished record, as Records. A dispute filed on it (POST /v1/fcra/orders/{id}/disputes) resolves deleted 60 seconds later, read on the next GET of the dispute; dispute.created and dispute.updated events are emitted. |
| Conviction-based exclusion | | The criminal search furnishes one exclusion-list record (recordType healthcare_exclusion, dispositionStatus conviction, authority 1128(a)(1)). A conviction-based exclusion is furnished under the default exclusions mode, conviction_only. |
| Conviction-based exclusion, exclusions none | | The same subject with exclusions: "none": no exclusion-list record is furnished or counted, so the search is clear. |
| Administrative exclusion, default mode | | An administrative exclusion (license action, not a conviction) is not furnished under the default conviction_only mode: the search is clear. |
| Administrative exclusion, exclusions all | | With exclusions: "all" the administrative exclusion is furnished: one record, dispositionStatus sanction, convictionBased false. |
A sandbox monitor is evaluated once, at creation, and its alert is POSTed to your channels.webhookUrl exactly once with the production signature; POST /v1/sandbox/monitors/{monitorId}/test-event sends one more on demand. Point it at the webhook tester to see the signed event and verify it in the browser, then follow the webhook example to do the same in your code.
Postman, SDKs and the Try-it console for the test API
Postman collection
Every sandbox request as a collection with a sandbox environment: download the collection or fork the public workspace.
SDKs and CLI
The Python and Node clients route an os_test_ key to the sandbox automatically; npx offendersearch search "Exact Match" --dob 1980-01-01 from a terminal. SDK reference ›
Try it in the docs
Every reference page has a console that runs the request against the sandbox with your test key and shows the response, then gives you the code. Sandbox docs ›
What the sandbox does not do
- No real people. Every record is fictional, on Sample, Example and Demo streets, with placeholder photographs. For match quality on real names, use the 25 free production calls.
- No simulated outages. Every source answers, complete, at once; the partial-result shape is documented in result completeness rather than reproduced here.
- No email. Monitor alerts go to your webhook only.
- No key-less access. A test key is free and takes seconds; it is how per-key limits stay fair.
Learn the API against the sandbox
Runnable guides Every example in the learning hub runs against this sandbox.
All guides: Learn APIs by example · reference: developer docs · packages: SDKs and CLI.
Public API for testing: questions
Is this a free public API for testing?
How is it different from a dummy API or a fake JSON API?
What is the test API URL?
Can I use it in CI?
Do the test scenarios cover webhooks?
Is there a public API without a key?
Does a test that passes in the sandbox pass in production?
Get a test key and send the first request
Free, no card. The sandbox answers in the production shape; swap the key when you ship.