Offendersearch API Keys API

The Keys API from Offendersearch API — 3 operation(s) for keys.

OpenAPI Specification

offendersearch-api-keys-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Offendersearch Keys API
  version: 1.0.0
  description: "National sex-offender search across all US states and territories, unified behind one API. Synchronous by default (call and wait) for interactive use; an asynchronous mode returns a job handle and is built for batch and high-volume work. Full offenders.io field parity plus scored matches, per-source provenance, freshness options, DOB/age match-state fidelity, and a consolidated verification-report PDF (`POST /v1/report`) with a source citation on every record. Criminal records land later as an additive `recordType` — the contract does not change.\n\n## Authentication\nMost customer endpoints authenticate with an API key sent in the `X-API-Key` header (`ApiKeyAuth`). The primary synchronous search additionally accepts a signed **session token** (`Authorization: Bearer <token>`) so the dashboard console can run searches on behalf of a signed-in user. The offenders.io compatibility endpoint accepts the key three ways for drop-in parity: `X-API-Key`, `Authorization: Bearer <key>`, or `?key=<key>`. Account/dashboard endpoints (`/v1/account`, `/v1/keys`, `/v1/usage`, ...) require a session token. Internal ops endpoints require the separate `X-Admin-Key` credential.\n\n## Freshness & billing\n`freshness` is a per-request parameter with two values — `daily` (the DEFAULT when omitted) and `weekly`.\n\n* **`daily`** is the freshest data we publish. Our sweep of every registry runs\n  on a daily cycle, so a `daily` answer is assembled from the newest snapshot we\n  hold of each one — in practice, almost real time. Ask for it when currency is\n  the point. It bills the **+$0.01/call daily-freshness surcharge** on top of the\n  base per-call rate (admin-overridable per customer).\n* **`weekly`** is also fresh. Identity is essentially identical to `daily` — the\n  same people, names, aliases, offenses, addresses and photos — and what can lag\n  is only the most recent movement, a registration or an address change from the\n  last day or so. Right for bulk screening and periodic re-screens. **No\n  surcharge.**\n\n\n**`freshness` describes the answer; it does not filter it.** No registry is ever withheld from a result because of when it was last swept: every registry covering the query contributes, from the newest snapshot we hold of it. `status` never becomes `partial` for a snapshot age, and `warnings[]` never carries a staleness sentence.\n\n**★ EVERY DATE IS ISO-8601 — BREAKING CHANGE, 2026-08-04.** Every date-shaped field in a record now comes back as ISO-8601: `dob`, the four `offense.*Date` fields, and the five `stateData` date fields. They used to be the registry's own string passed through byte-for-byte, so a single response could carry `\"2003-03-31\"`, `\"10/11/1988\"`, `\"11-29-1987\"` and `\"Aug. 10, 1987\"` in the same key — `offense.offenseDate` was ISO on only 45.4% of its populated values, in seven distinct shapes. **If you wrote a lenient parser or a per-state format table, you can delete it; if you compared these values as raw strings, or stored them in a text column and matched on it, those comparisons will change once.**\nThree rules govern the new values, and the third is the one to read carefully:\n1. A full date is `YYYY-MM-DD`. 2. **A partial date stays partial.** A registry that publishes only a month or\n   only a year yields `YYYY-MM` or `YYYY` — we never invent a day. The companion\n   `datePrecision` object on `offense` and `stateData` names the precision\n   explicitly, exactly as `dobPrecision` has always done for `dob`.\n3. **Nothing is discarded.** A value we cannot read as a date — a crime\n   description in an offence-date cell, a `registrationEnds` of \"Life\" — is NOT\n   served as a fake date and NOT silently blanked. The field is `\"\"`, the\n   precision is `unparseable`, and the registry's literal text is preserved in\n   the `datesAsPublished` object beside it.\n\n`source.scrapedAt` / `.lastCheckedAt` / `.sourceUpdatedAt` were already ISO-8601 timestamps and are unchanged. `dob` was already `YYYY-MM-DD` and is unchanged.\n\n**There is no date FILTER on the offense or stateData dates.** `dob` is the one date you can narrow a query on; read the rest off the record.\n**Provenance is never traded for normalisation.** The registry's own text is kept beside the normalised value, not overwritten by it, which is why `datesAsPublished` still carries what the source actually printed — including the 38,818 `registrationEnds` cells that hold a registration DURATION (\"15 Years\", \"Lifetime\") rather than a date.\n\n**Where currency is published — and where it is not.** Per record, `record.source.scrapedAt` is the moment we ingested the snapshot that record came from. Per registry, `GET /v1/sources` publishes `health.lastSuccessAt` and `health.ageSeconds` live and without an API key. Both are things you ask for. Neither is attached to a search answer: `sourceStatus[]` carries no age fields, because an answer that contains every matching record does not need a caveat about our sweep schedule.\n\n★ **Freshness and completeness are different questions, and only one of them makes an empty result unsafe.** `freshness` says how OLD an answer is; it says nothing about whether the answer is WHOLE. A registry swept ten minutes ago can still fail to be searched to the end — a deadline, or a candidate set larger than one search may examine — and then a `0` from it means UNKNOWN, not NO MATCH. That is `counts.sourcesIncomplete` / `sourceStatus[].incomplete`, it is unrelated to freshness, and it is the one signal that must never be ignored. See API-CONTRACT.md §5.0.\n\n**Migrating from a pre-2026-08-04 integration.** `counts.sourcesStale`, `counts.sourcesDegraded`, `counts.sourcesOmitted`, `counts.recordsFromStaleSources`, `freshnessDetail`, and `sourceStatus[].freshnessSatisfied` / `.degraded` / `.omitted` / `.ageSeconds` / `.lastSuccessAt` were removed and are no longer emitted. If you were gating on any of them, gate on `counts.sourcesIncomplete` instead — it asks whether your ANSWER is whole, which is the question those keys were being used to ask. `onStale` is deprecated but still accepted, so an older request body keeps working.\n\nBilling is required for every account; the ONLY free call is one scoped exclusively to statutorily non-commercial jurisdiction(s) (e.g. state=CA), which has nothing commercially billable. **Batch billing is per-search:** `POST /v1/batch` is one HTTP request but each search in it is billed as its own search (per-row freshness included) — a batch of 100 = 100 billable searches. The consolidated verification report (`POST /v1/report`) is a separate, callable endpoint any valid key may use (no per-key entitlement), billed **+$0.02 per document** (admin-overridable per customer). A separate `proof` add-on also carries an extra per-document charge and requires billing (402 otherwise); it is NOT the customer verification report.\n"
servers:
- url: https://api.offendersearch.app
security:
- ApiKeyAuth: []
tags:
- name: Keys
paths:
  /v1/keys:
    get:
      operationId: listKeys
      summary: List API keys for the account (session)
      security:
      - BearerAuth: []
      responses:
        '200':
          description: The account's API keys (masked; secrets are never re-shown).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKey'
        '401':
          $ref: '#/components/responses/Unauthorized'
      tags:
      - Keys
    post:
      operationId: createKey
      summary: Create an API key (secret shown once)
      security:
      - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateKeyBody'
      responses:
        '201':
          description: Key created. `secret` is returned ONCE here; only its hash is stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyWithSecret'
        '401':
          $ref: '#/components/responses/Unauthorized'
      tags:
      - Keys
  /v1/keys/{keyId}/rotate:
    post:
      operationId: rotateKey
      summary: Rotate an API key (new secret shown once)
      security:
      - BearerAuth: []
      parameters:
      - name: keyId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Rotated key; new `secret` returned once.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyWithSecret'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Key not found or revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      tags:
      - Keys
  /v1/keys/{keyId}:
    delete:
      operationId: deleteKey
      summary: Revoke an API key
      security:
      - BearerAuth: []
      parameters:
      - name: keyId
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Key revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - revoked
                  id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Key not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      tags:
      - Keys
components:
  responses:
    Unauthorized:
      description: Missing or invalid credential.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    CreateKeyBody:
      type: object
      properties:
        name:
          type: string
          default: Untitled key
        freshness:
          type: array
          items:
            type: string
            enum:
            - standard
            - weekly
            - daily
          nullable: true
          description: LEGACY/UNENFORCED. Freshness is a PER-REQUEST parameter on /v1/search and is NOT provisioned on the key — any valid key may send `daily` or `weekly` and is billed accordingly. This field is retained for compatibility and does not gate anything.
        proof:
          type: boolean
          nullable: true
          description: Whether the key may request proof docs. Defaults true.
    ApiKeyWithSecret:
      allOf:
      - $ref: '#/components/schemas/ApiKey'
      - type: object
        properties:
          secret:
            type: string
            description: Full key secret — returned ONCE on create/rotate; only its hash is stored.
    Error:
      type: object
      description: Standard FastAPI error body.
      properties:
        detail:
          type: string
          description: Human-readable error message.
          example: Missing or invalid API key. Send it in the X-API-Key header.
    ApiKey:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        maskedKey:
          type: string
          description: Prefix + ellipsis; the full secret is never re-shown.
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum:
          - active
          - revoked
        scopes:
          type: object
          properties:
            freshness:
              type: array
              items:
                type: string
                enum:
                - standard
                - weekly
                - daily
            proof:
              type: boolean
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Customer API key. Primary auth for search/records/compat/proof.
    BearerAuth:
      type: http
      scheme: bearer
      description: 'Signed session token (HMAC-SHA256). Auth for account/dashboard endpoints; also accepted by POST /v1/search and the compat endpoint.

        '
    QueryKeyAuth:
      type: apiKey
      in: query
      name: key
      description: API key passed as `?key=` — offenders.io demo mode (compat endpoint only).
    AdminAuth:
      type: apiKey
      in: header
      name: X-Admin-Key
      description: Internal admin credential — separate from customer API keys.