# Retrieving individual records

> A search returns records; this is how you get one of them back later, which identifier to use, and what each identifier is for.

- **HTML:** https://offendersearch.app/docs/guides/sex-offender/retrieving-records
- **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

## When to use this

A reviewer opens a hit from yesterday's queue; an audit asks what a cited record said; a reconciliation job checks that the person you flagged is still the same person. Each needs one record by identifier rather than a new search. `GET /v1/records/{recordId}` resolves a record by either identifier a search published for it — our `recordId` or the registry's own `uuid` — and is not billed. You will build the lookup, decide which identifier to store for which purpose, and handle the two non-200 answers.

## Before you start

- A test key (API key only on this route). Run a sandbox search for **Exact Match** first; the record carries `recordId` `rec_28db9639d712c4c61e8e`, `personRef` `prs_28db9639d712c4c61e8e` and `uuid` `SBX-OH-100001`.
- Three identifiers, three jobs: **`personRef`** (`prs_` + 20 hex) is the stable key for the person and what a clearance names; **`recordId`** (`rec_` + 20 hex) is the handle for this result and what this route and a report cite; **`uuid`** is the registry's own number, unique only together with its jurisdiction.
- A `rec_` id resolves while the search that returned it is retained — 14 days — and from the platform's record index; a registry `uuid` resolves from the current data, and a superseded `uuid` still resolves to the person's **current** record.

> **The one thing people get wrong.** Do not key a person on `recordId`. It describes the merge that produced the record for that query, so the same person can carry a different `recordId` on a scoped search and on a full search. Key the person on `personRef`; keep `recordId` to cite a specific result.

## Step by step

1. **Search, and keep three identifiers.** From the **Exact Match** record store `personRef`, `recordId` and `uuid` with the jurisdiction (`sources[].jurisdiction`).
2. **Fetch by `recordId`.** `GET /v1/records/rec_28db9639d712c4c61e8e`. The full `Record` comes back; the match fields are empty because nothing was matched.
3. **Fetch by registry `uuid`.** `GET /v1/records/SBX-OH-100001` returns the same record. In production a `uuid` shared by several jurisdictions answers `409`.
4. **Handle `404` and `409`.** A `404` means this identifier was never issued for a record, or the search that issued it has aged out — never that the person is unregistered. A `409` means the `uuid` is ambiguous; use `recordId`.

### Steps 2–3 — the lookups

**cURL**
```bash
curl -s https://sandbox.offendersearch.app/v1/records/rec_28db9639d712c4c61e8e -H "X-API-Key: $OFFENDERSEARCH_KEY"
curl -s https://sandbox.offendersearch.app/v1/records/SBX-OH-100001          -H "X-API-Key: $OFFENDERSEARCH_KEY"
```

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

BASE = os.environ.get("OFFENDERSEARCH_BASE", "https://sandbox.offendersearch.app")
H = {"X-API-Key": os.environ["OFFENDERSEARCH_KEY"]}

def get_record(identifier):
    r = requests.get(BASE + "/v1/records/" + identifier, headers=H, timeout=30)
    if r.status_code == 404:
        return None                        # identifier never issued, or aged out — NOT "unregistered"
    if r.status_code == 409:
        raise ValueError("ambiguous registry uuid; use the recordId: " + r.json()["detail"])
    r.raise_for_status()
    return r.json()

rec = get_record("rec_28db9639d712c4c61e8e")
print(rec["personRef"], rec["name"]["full"], rec["registrationState"], rec["stateData"] is not None)
same = get_record("SBX-OH-100001")
assert same["personRef"] == rec["personRef"]
```

**JavaScript**
```javascript
const BASE = process.env.OFFENDERSEARCH_BASE ?? 'https://sandbox.offendersearch.app';
const H = { 'X-API-Key': process.env.OFFENDERSEARCH_KEY };

async function getRecord(identifier) {
  const res = await fetch(BASE + '/v1/records/' + encodeURIComponent(identifier), { headers: H });
  if (res.status === 404) return null;                 // never issued or aged out — NOT "unregistered"
  if (res.status === 409) throw new Error('ambiguous registry uuid; use the recordId: ' + (await res.json()).detail);
  if (!res.ok) throw new Error('HTTP ' + res.status);
  return res.json();
}

const rec = await getRecord('rec_28db9639d712c4c61e8e');
console.log(rec.personRef, rec.name.full, rec.registrationState, rec.stateData !== null);
const same = await getRecord('SBX-OH-100001');
console.log(same.personRef === rec.personRef);         // true
```

*GET /v1/records/rec_28db9639d712c4c61e8e → 200 (trimmed)*
```json
{
  "recordId": "rec_28db9639d712c4c61e8e",
  "personRef": "prs_28db9639d712c4c61e8e",
  "uuid": "SBX-OH-100001",
  "recordType": "sex_offender",
  "matchConfidence": null,
  "matchBasis": [],
  "matchDetail": { "strategies": {}, "fieldsPresent": [], "signals": [], "matchedNameType": null, "verification": null },
  "matchedName": null,
  "name": { "first": "Exact", "middle": "Sample", "last": "Match", "suffix": "", "full": "Exact Sample Match" },
  "dob": "1980-01-01", "birthYear": 1980, "dobPrecision": "exact", "age": "46",
  "addresses": [ { "type": "residence", "line1": "100 Sample St", "city": "Sampleville", "county": "Sample", "state": "OH", "zipcode": "00000", "lat": null, "lng": null } ],
  "registrationState": "OH", "addressStates": ["OH"],
  "offenses": [ { "crime": "Sexual battery", "statute": "SAMPLE 2907.03", "tier": "Tier III", "convictionDate": "2004-05-12" } ],
  "stateData": { "status": "Active", "designation": "Tier III sex offender", "registrationDuration": "Lifetime",
                 "isLifetimeRegistration": true, "complianceStatus": "compliant", "lastVerificationDate": "2026-09-01",
                 "lawAgency": "Sample County Sheriff's Office" },
  "flags": { "absconder": false, "predator": false },
  "images": [ { "url": "https://sandbox.offendersearch.app/v1/sandbox/photos/1.svg" } ],
  "sources": [ { "jurisdiction": "OH", "registryName": "Sandbox sample registry (OH)", "recordUrl": "https://offendersearch.app/docs/sandbox", "lastCheckedAt": "2026-10-01T00:00:00+00:00" } ],
  "matchState": null, "unverified": false,
  "sandbox": true
}
```

Two things differ from the search copy. The match fields are empty (`matchConfidence: null`, `matchBasis: []`, `matchState: null`): a fetch matches nothing, so there is nothing to explain. And `stateData` is populated here exactly as on a search — the full record is the default on both.

### Step 4 — the two refusals

**404**
```json
{ "detail": "Not found. A recordId resolves while the search that returned it is retained; run the search again to get a current recordId, or look the person up by registry uuid and state." }
```

**409 (production, wording abridged)**
```json
{ "detail": "Identifier '20059' is not unique — it matches 3 records across FL, PA, WI. Use the recordId from the search result, or run POST /v1/search with the registry named in jurisdictions." }
```

Registries re-issue their own numbers; one `uuid` can identify a registrant in Florida, another in Pennsylvania and a third in Wisconsin. The route returns one record and will not guess, so the `409` sends you to `recordId`, which is namespaced per jurisdiction and does not collide. The sandbox's `uuid`s are unique, so you will not see a `409` there.

## Reading the result

| Identifier | Shape | Store it for | Never use it for |
| --- | --- | --- | --- |
| `personRef` | `prs_` + 20 hex | Recognising the same person next time; clearances; monitors. Stable across re-issued registry numbers. | Citing a specific result. |
| `recordId` | `rec_` + 20 hex | Citing this result in a report, dispute or audit line; `GET /v1/records/{recordId}`. | Keying a person; it differs between query scopes. |
| `uuid` + `sources[].jurisdiction` | registry's own string, may be `""` | Cross-referencing the registry's own listing; a superseded one still resolves. | Lookups without the jurisdiction — it collides across states. |
| `sources[].recordUrl` | URL or `""` | A link for a reviewer to verify at the source. | De-duplicating or diffing. |

Every top-level key is present on every record; `""` means the registry does not publish the field, `null` means unknown or not applicable, `[]` means none. The full 37-key list and the empty-value rules are in [the Record object](/docs/record-object).

## Edge cases and pitfalls

- **`404` is about the identifier, not the person.** After 14 days a `rec_` id from a search can stop resolving; run the search again for a current one. Never show "not registered" on a `404`.
- **A `rec_` copy can be light.** The record a `rec_` id returns may be the copy a previous search served, without `stateData` when that search did not ask for it. Read `stateData` as "not requested" when `null` on a fetch of a light copy.
- **Ambiguity is real in production**: tens of thousands of registry numbers are shared across jurisdictions. Prefer `recordId` for lookups you automate.
- **Control characters and ids over 256 characters answer `404`**, never a `5xx`.
- **To re-open the whole search**, not one record, use `GET /v1/searches/{searchId}/results` (stored records, 14 days, up to 200).
- **Photos.** `images[].url` on some registries points at the platform's media route, which also needs your key.

## Production checklist

- `personRef` is the primary key for people in your own tables; `recordId` and `searchId` are stored per decision.
- Lookups by `uuid` always carry the jurisdiction in your own data and tolerate a `409`.
- `404` renders as "record no longer available", never as a clearance.
- Re-fetches happen within the 14-day window, or a new search is run.

## Related

- [The Record object](https://offendersearch.app/docs/record-object)
- [Clearances](https://offendersearch.app/docs/clearances)
- [Asynchronous searches](https://offendersearch.app/docs/guides/sex-offender/async-searches)
- [Platform: data model](https://offendersearch.app/docs/platform/data-model)
