# Postman API documentation: public workspace, live sandbox

> A collection generated from the OpenAPI contract, an environment pointed at a free sandbox, and the requests pre-filled with names the sandbox answers — import both and send.

- **HTML:** https://offendersearch.app/docs/learn/postman-api-documentation
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## What Postman is for when you work with an API

Postman is an HTTP client with a memory. A **request** is one call with its method, URL, headers and body; a **collection** is a folder of requests that share variables and auth; an **environment** is a set of variable values (host, key) you can switch without editing a request. Add a **Tests** script and a collection becomes a runnable check; add descriptions and saved examples and it becomes documentation Postman can publish.

For an API like this one it answers three questions quickly: *does my key work?*, *what does this endpoint actually return?*, and *what does the request look like in my language?* — the code snippet panel translates any request into curl, Python, JavaScript and more.

## Import the OffenderSearch collection and environment

Two files, both generated from the published OpenAPI document so they cannot drift from the API:

- [offendersearch.postman_collection.json](https://offendersearch.app/postman/offendersearch.postman_collection.json): Every public endpoint in eight folders — Sex Offender Search, Criminal Records, Monitoring, Map API, Clearances, FCRA Partner API, FCRA consumer portal, Account & support — with request bodies taken from the sandbox's own scenarios.
- [sandbox.postman_environment.json](https://offendersearch.app/postman/sandbox.postman_environment.json): `base_url` set to the sandbox host and `api_key` as a secret variable you fill in.

1. **Import both files.** Postman → **Import** → drop the two files (or paste their URLs). The collection lands in your workspace; the environment appears in the environment selector.
2. **Create a test key.** [Dashboard → API keys](/dashboard/keys) → **Create test key**. It begins `os_test_`, works only on the sandbox and is never billed. 25 free live calls come with the account too, but the sandbox is where you learn.
3. **Set api_key in the environment.** Select the **OffenderSearch sandbox** environment, open it, paste the key into `api_key` (it is a secret-type variable, masked in the UI and excluded from exports).
4. **Send.** Open **Sex Offender Search → Synchronous search** and press Send. The body is already a name the sandbox matches.

> **Public workspace.** When the collection is published to Postman's public API network, the workspace link lands on this page and on [/postman](/postman). Until then the files above are the collection; forking them into your own workspace is the same result.

## Postman environment variables: base_url and api_key

*The sandbox environment, trimmed*
```json
{
  "name": "OffenderSearch sandbox",
  "values": [
    { "key": "base_url", "value": "https://sandbox.offendersearch.app", "type": "default", "enabled": true },
    { "key": "api_key",  "value": "os_test_...",                         "type": "secret",  "enabled": true }
  ]
}
```

| Variable | Sandbox | Production | Notes |
| --- | --- | --- | --- |
| `base_url` | `https://sandbox.offendersearch.app` | `https://api.offendersearch.app` | Every request URL starts with `{{base_url}}` |
| `api_key` | an `os_test_…` key | an `os_live_…` key | Sent as `X-API-Key`; a key on the wrong host answers `401` |
| `consumer_portal_token` | from `POST /v1/consumer/portal/session` | same | Only the four FCRA consumer-portal requests use it |

Duplicate the environment, rename it **production**, change the two values, and switching environments switches hosts with no request edited. Keep `api_key` a *secret* variable so it never lands in an exported file or a shared workspace — the lifecycle of the key itself is on the [API token example](/docs/learn/api-token-example) page.

## Send a sandbox request and read the answer

The sandbox answers fixed names with fixed outcomes, so a request is also a known-good check. The collection's first search uses the name that returns one date-of-birth-verified match:

*Sex Offender Search → Synchronous search*
```http
POST {{base_url}}/v1/search
X-API-Key: {{api_key}}
Content-Type: application/json

{
  "query": { "firstName": "Exact", "lastName": "Match", "dob": "1980-01-01" }
}
```

*What the sandbox returns (trimmed)*
```json
{
  "status": "complete",
  "counts": { "records": 1, "recordsReturned": 1, "sourcesQueried": 58, "sourcesComplete": 58, "sourcesIncomplete": 0 },
  "records": [
    {
      "matchState": "dob_match",
      "matchedName": { "value": "Exact Match", "type": "legal" },
      "matchConfidence": 0.9,
      "registrationState": "OH"
    }
  ],
  "sandbox": true
}
```

Change the last name to anything not on the [sandbox list](/docs/sandbox) and the same request returns `status: complete` with zero records — the other branch every integration must handle. Send `Felony Record` with `dob` `1975-06-15` to **Criminal Records → Search criminal records** for one felony court case, and **Monitoring → Create a monitor** for a `201` with an immediate alert. Every scenario and its expected fields are listed on the sandbox page and in the collection's request descriptions.

## Postman tests: turn a request into a check

The **Tests** tab runs JavaScript after a response arrives. Because the sandbox is deterministic, a test can assert exact values, which makes the collection a smoke test of your key, your network and the API in one click of the Collection Runner.

*Tests for the synchronous search request*
```javascript
// Tests tab — runs after the response arrives
pm.test("200 OK", () => pm.response.to.have.status(200));

const body = pm.response.json();
pm.test("the answer is complete", () => pm.expect(body.status).to.eql("complete"));
pm.test("one DOB-verified match", () => {
  pm.expect(body.counts.records).to.eql(1);
  pm.expect(body.records[0].matchState).to.eql("dob_match");
});

// Hand the id to the next request in the collection
pm.collectionVariables.set("searchId", body.searchId);
```

Chaining is the second use: store an id from one response in a collection variable and the next request reads it — the search id into the verification report request, the monitor id into its alerts request. The Runner executes the folder in order with the variables flowing through.

## API documentation in Postman: how a collection becomes docs

Postman renders a collection as documentation: the collection description becomes the overview, each folder description a section, each request's description its page, with the method, URL, headers, body and **saved example responses** shown beside it, and the code snippet in the reader's chosen language. Publishing makes that page public at a Postman URL; a public workspace adds forking and version history.

1. **Describe at every level.** Collection, folder and request descriptions are Markdown. The generated collection carries each endpoint's summary and description from the OpenAPI document; add your own notes on top.
2. **Save example responses.** Send a request, then **Save as example**. Documentation with a real response is documentation someone can trust.
3. **Keep secrets out.** Use `{{api_key}}` everywhere and keep the real value in a secret environment variable. Published docs show the placeholder.
4. **Publish.** **View documentation → Publish**, or make the workspace public. Re-publishing after an import keeps the URL.

What Postman documentation is good at: one request, one response, one snippet per language, always current with the collection. What it is not: concept pages, guides and the error contract — those live in a portal like [this one](/docs/learn/api-documentation-example), which is why the collection links back to the docs on every folder.

## Create a collection from an OpenAPI document

You do not have to start from ours. **Import → paste a URL** and give Postman [/openapi.yaml](/openapi.yaml): it generates a collection with every path, parameter and schema-derived body. That is how the published collection is built, with two additions a plain import does not make — the request bodies are the sandbox's real scenario names rather than schema placeholders, and the header, variables and folder notes are set for the sandbox-first workflow. The generator is a small script; re-running it after an API change regenerates the files, and a test fails when the committed files are stale.

- After importing any OpenAPI document, set the collection's **Authorization** once (API key, header `X-API-Key`, value `{{api_key}}`) and let requests inherit it.
- Replace schema placeholder bodies with values the API actually accepts; a `"string"` where a date goes teaches nothing.
- Name the environment after the host, not the project — `sandbox`, `production` — so the selector reads as a switch.

## Postman vs curl vs an SDK

The same request, three ways. Postman is for exploring and documenting; curl is for scripts and bug reports; an SDK is for the code you ship.

**curl**
```bash
curl -X POST https://sandbox.offendersearch.app/v1/search \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": {"firstName": "Exact", "lastName": "Match", "dob": "1980-01-01"}}'
```

**Python**
```python
import os, requests

r = requests.post(
    "https://sandbox.offendersearch.app/v1/search",
    headers={"X-API-Key": os.environ["OFFENDERSEARCH_API_KEY"], "Content-Type": "application/json"},
    json={"query": {"firstName": "Exact", "lastName": "Match", "dob": "1980-01-01"}},
    timeout=60,
)
r.raise_for_status()
print(r.json()["records"][0]["matchState"])   # dob_match
```

**Node**
```javascript
const res = await fetch("https://sandbox.offendersearch.app/v1/search", {
  method: "POST",
  headers: { "X-API-Key": process.env.OFFENDERSEARCH_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ query: { firstName: "Exact", lastName: "Match", dob: "1980-01-01" } }),
});
const data = await res.json();
console.log(data.records[0].matchState); // dob_match
```

|  | Postman | curl | SDK |
| --- | --- | --- | --- |
| Best for | Exploring responses, sharing a reproducible request, docs | One-liners, CI, pasting into a support ticket | Production code with types, retries and pagination handled |
| Auth handling | Collection-level, variables | A header flag | Reads `OFFENDERSEARCH_API_KEY`, picks the host from the prefix |
| Reuse | Collections and environments | Shell history | A package in your dependency file |
| Where to get it | The files above | Any example on this site has a curl tab | [Python and Node SDKs](/sdk) |

## Postman FAQ

### How do I use the postman api documentation for OffenderSearch?

Import the collection and environment files from this page, set `api_key` to a test key, and send any request. Each request's description is the endpoint's documentation; the full docs are at /docs.

### How do I create api documentation with postman?

Write Markdown descriptions on the collection, folders and requests, save example responses, keep secrets in environment variables, then View documentation → Publish. Postman renders a page per request with the snippet in the reader's language.

### Can I generate a postman collection from an api?

From its OpenAPI document, yes: Import → URL → the openapi.yaml. Every path becomes a request. Then fix the bodies and set collection-level auth.

### Is there a public postman workspace for this API?

The collection and environment are published as files here. When a public workspace on Postman's API network is live, its link is added to this page and to /postman.

### Why does my request return 401 in Postman?

The environment is not selected, `api_key` is empty, or the key's prefix does not match `base_url` — `os_test_` keys work only on the sandbox host, `os_live_` only on the live host. Check the environment selector in the top right first.

### Does sending requests from Postman cost anything?

Against the sandbox, never. Against production with a live key, each call is billed at the published rate ($0.15 a call) after the account's 25 free calls.

## What this page does not cover

- Postman's team features — workspaces, roles, monitors, mock servers, the Postman API itself. Its own documentation covers those.
- Newman and CLI runs of the collection in CI. The collection works with them unchanged; the setup is Postman's to document.
- The endpoints' semantics. The collection links each folder to the product documentation: [sex offender](/docs/sex-offender), [criminal](/docs/criminal), [monitoring](/docs/monitoring), [map](/docs/map), [FCRA](/docs/fcra).

## Related

- [Postman collection and sandbox environment (download page)](https://offendersearch.app/postman)
- [Public API for testing: the sandbox](https://offendersearch.app/sandbox)
- [The sandbox's fixed test names and outcomes](https://offendersearch.app/docs/sandbox)
- [API documentation example: what good docs look like](https://offendersearch.app/docs/learn/api-documentation-example)
- [API token example: create, scope, rotate and revoke a key](https://offendersearch.app/docs/learn/api-token-example)
- [RESTful API example: one request, one response](https://offendersearch.app/docs/learn/restful-api-example)
- [API errors: designing codes a client can act on](https://offendersearch.app/docs/learn/api-errors)
- [The OpenAPI document](https://offendersearch.app/openapi.yaml)
- [Python and Node SDKs](https://offendersearch.app/sdk)
- [Background check API](https://offendersearch.app/background-check-api)
- [Criminal records API](https://offendersearch.app/criminal-records-api)
- [All learning guides](https://offendersearch.app/docs/learn)
