---
title: "Get catalog gaps and integrity checks"
method: GET
path: "/v1/data/gaps"
tags: ["Data Quality"]
---

# Get catalog gaps and integrity checks

`GET /v1/data/gaps`

## Request

- Base URL: `https://api.gunspec.io`
- URL: `https://api.gunspec.io/v1/data/gaps`
- Auth: one of:
  - none
  - API key in header `X-API-Key`
  - HTTP bearer

Two kinds of problem, reported separately.

**Gaps** are absent values, counted per entity and field, with the share of records left filled. **Media** is the same question for visual material, which a field count cannot answer: a line drawing is a column but photographs, renders and 3D models are rows in a media table. **Checks** are present values that contradict something else the catalog states, such as a firearm attributed to a manufacturer that does not exist, or a company recorded as closing before it was founded. Each check returns a verdict and a sample of the identifiers that failed.

Built for a backend rather than a reader. `/v1/data/coverage` reports how full a table is; this reports which records to correct. Pair it with `/v1/data/gaps/records` to retrieve the identifiers and pass them to whatever fills them in.

Pass `entity` to report on a single catalog. Every check is a scan of a catalog table, so a scoped call runs only the checks that concern that catalog and is substantially cheaper.

`linkHealth` is declared and reports `unavailable`. Establishing whether a cited website still resolves requires one network request per record, which belongs in a scheduled job rather than a request handler. It is declared here so that a consumer knows the shape it will arrive in.

No API key required.

## Query parameters

- `entity` 'manufacturers' | 'firearms' | 'calibers' | 'attachments' | 'ammunition'

## Response `200`

Get catalog gaps and integrity checks

- object
  - `success` boolean, required: Always true on a successful response. Every error carries `false` and an `error` object instead.
  - `data` object, required: The response payload.
    - `scope` string: Which catalog this report covers, or `all`. Echoed so a stored copy still says what it describes.
    - `schedule` object: Two different times, deliberately separate. The gap report is recomputed on every request, so `computedAt` describes this body. The daily snapshot is what has a last run and a next run, and it is what `/v1/data/gaps/history` records.
      - `computedAt` string: When this response was computed. Not when you read it: the response is edge-cached for a minute, so a body can be up to that old.
      - `cadence` string: How often the recorded snapshot is taken.
      - `lastRunAt` string, nullable: When the last snapshot was stored, or null before the job has ever run.
      - `lastRunOn` string, nullable: The UTC day that snapshot describes.
      - `nextRunAt` string: When the next snapshot is due.
      - `sourceAuditAt` string, nullable: When the daily source audit last judged the catalog's citations, or null before it has ever run. Its own time rather than the snapshot's: `record-cites-an-authority` reads a table a separate job writes, and the two run in parallel.
    - `entities` object: Keyed by entity. Each carries its total and, per tracked field, how many records lack it. A field is missing when it is null **or empty**, so a column full of empty strings does not read as complete.
    - `media` object[]: Visual coverage, one entry per catalog that stores media - firearms today, and an empty array for a scope that stores none. A catalog with no pictures and a catalog that does not hold pictures are different answers, so absence is never reported as zero coverage. Each kind is counted twice: `records` is how many records have at least one asset of that kind, which is the coverage figure, and `assets` is how many files exist, which is larger wherever a record carries several. `none` is how many records have nothing to show at all. This is what field coverage cannot report. A firearm's line drawing is a column, but its photographs, renders and 3D models are rows in a media table, so "does this record have a render" is not a question a field count can answer.
      - `entity` string: Which catalog this covers.
      - `total` number: How many records that catalog holds.
      - `kinds` object: Keyed by kind: `silhouette`, `photo`, `render`, `model`, `schematic`. Each carries `records`, `assets` and `coveragePct`.
      - `none` number: Records with no drawing, photograph, render, model or schematic.
    - `checks` object[]: One entry per integrity check, each naming what passing means so a failure is actionable without reading the source.
      - `id` string, required: Stable id, usable at /v1/data/gaps/checks/{checkId}.
      - `describes` string, required: What passing means.
      - `status` 'pass' | 'fail', required: Production status, one of `in_production`, `discontinued`, `out_of_production`, `in_service`, `limited_production`, `prototype`. Discontinued records are kept deliberately: a rifle built in 2004 wears parts nobody sells today.
      - `found` number, required: How many records failed.
      - `sample` string[], required: Up to five failing ids. Ask the check endpoint for the rest.
      - `values` object[]: The distinct values behind the failure, present only on a check about a value rather than a record. A vocabulary failure is decided once per spelling rather than once per record, so thirty-four records reporting nineteen spellings are nineteen decisions, and ids alone would mean fetching thirty-four records to recover them. Absent on every other check rather than empty, so a check with no values to report and a check that found none cannot be confused.
        - `value` string: The value the column holds.
        - `count` number: How many rows hold it.
    - `linkHealth` object: Reserved for whether each cited website still resolves, which a scheduled job will fill. Reports `unavailable` with a reason until then.

## Other responses

- `400`: Validation error
- `429`: A limit was reached. `RATE_LIMITED` is the per-minute limit: wait `Retry-After` seconds (a minute) and retry. `DAILY_CAP_EXCEEDED` is the plan's daily limit, per key: nothing will be served until the counters return to zero at midnight UTC. `MONTHLY_CAP_EXCEEDED` is the plan's monthly allowance, for the whole account: nothing will be served until midnight UTC on the first of next month, or until the plan changes. For both, `Retry-After` is the seconds until then and `error.resetsAt` says when as an instant. Calls made after a refusal are refused too and do not spend the month, and a free key that keeps calling is paused, so stop at the first one and resume at the reset.
- `500`: Internal server error

---

[API](https://skmtc.dev/gunspec/apis/gunspec-io-api.md) · [All operations](https://skmtc.dev/gunspec/apis/gunspec-io-api/llms.txt) · [OpenAPI document](https://skmtc.dev/gunspec/apis/gunspec-io-api/revisions/f2e974d8b05c?raw)
