# Firearms

Resource class for interacting with the GunSpec Firearms API. Wraps all `/v1/firearms` endpoints. Instantiated internally by the GunSpec client and exposed as `client.firearms`.

Source: https://docs.gunspec.io/en/sdk/firearms

## client.firearms.list()

`GET /v1/firearms`

Auth: API key required  
Tier: Explorer+

List firearms with optional filters and pagination.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `sort` | string | no | Sort field |
| `order` | string | no | Sort order |
| `fields` | string | no | Comma-separated list of fields to include |
| `manufacturer` | string | no | Filter by manufacturer slug |
| `caliber` | string | no | Filter by caliber slug |
| `category` | string | no | Filter by category slug |
| `action_type` | string | no | Filter by action type, as stored and underscored. The vocabulary is /v1/firearms/action-types |
| `country_of_origin` | string | no | Filter by ISO 3166-1 alpha-2 country code |
| `year_introduced_min` | integer | no | Minimum year introduced |
| `year_introduced_max` | integer | no | Maximum year introduced |
| `weight_min` | number | no | Minimum empty weight (grams) |
| `weight_max` | number | no | Maximum empty weight (grams) |
| `barrel_length_min` | number | no | Minimum barrel length (mm) |
| `status` | string | no | Filter by production status |
| `features` | string | no | Comma-separated feature slugs; all must match |
| `has_3d_model` | string | no | Only firearms with (or without) a 3D model |
| `has_image` | string | no | Only firearms with (or without) real imagery: a render or a photograph, ignoring the line-art silhouette every firearm has |
| `created_after` | string | no | Only records added after this ISO 8601 timestamp |
| `created_before` | string | no | Only records added before this ISO 8601 timestamp |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmListItem[] | One page of results |
| `data[].id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. Stable: it is what a mirror keys on. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].status` | FirearmStatus \| null | 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. |
| `data[].countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data[].actionType` | string \| null | How the action cycles: `short_recoil`, `gas_operated`, `blowback`, `bolt_action` and so on. An open vocabulary: the values in use are published on the schema as `x-gunspec-vocabulary`, and `GET /v1/firearms/action-types` is the live list. |
| `data[].weightEmptyG` | number \| null | Unloaded weight in grams, without magazine. |
| `data[].barrelLengthMm` | number \| null | Barrel length in millimetres, measured from the breech face. |
| `data[].images` | InlineMediaItem[] | Every image this firearm has, silhouette first. Empty when none are on file. |
| `data[].images[].id` | integer | Autoincrementing image id. |
| `data[].images[].url` | string | Absolute, directly fetchable URL. |
| `data[].images[].kind` | MediaKind | What kind of asset a media row is. |
| `data[].images[].alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data[].images[].width` | integer \| null | Pixel width, where it is known. |
| `data[].images[].height` | integer \| null | Pixel height, where it is known. |
| `data[].createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data[].svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data[].model3dUrl` | string \| null | GLB model, or null where none is on file. |
| `data[].favoriteCount` | integer | How many accounts have favorited this record. |
| `data[].updatedAt` | string | When a field we serve last changed. Maintained by the database rather than by whatever wrote the row, so it moves for a corrected specification and does not move for a re-run of the importer. |
| `data[].version` | string \| null | Opaque fingerprint of this record. It changes when and only when a field we serve changes, so a client holding the same value already has the record and need not fetch it. Equality is the only supported operation: do not parse it, and do not assume how it is computed. Null means the record has not been written since versioning began, not that it is unchanged. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "glock-g17-gen5",
      "name": "Glock 17 Gen5",
      "manufacturerId": "glock",
      "categoryId": "pistol",
      "yearIntroduced": 2017,
      "status": "in_production",
      "countryOfOrigin": "AT",
      "actionType": "short_recoil",
      "weightEmptyG": 625,
      "barrelLengthMm": 114,
      "images": [
        {
          "id": 2572,
          "url": "https://api.gunspec.io/v1/firearms/ak-47/images/2572",
          "kind": "silhouette",
          "alt": "AK-47 right profile",
          "width": 1536,
          "height": 1024
        }
      ],
      "createdAt": "2026-09-06 23:55:07",
      "svgLineArtUrl": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/media/silhouette",
      "model3dUrl": null,
      "favoriteCount": 0,
      "updatedAt": "2026-09-10 06:42:19",
      "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms?category=pistol' \
  --header 'X-API-Key: your_key'
```

## client.firearms.listAutoPaging()

`GET /v1/firearms`

Auth: API key required  
Tier: Explorer+

Auto-paginate through all firearms matching the given filters. Returns an async iterator that fetches pages on demand, yielding individual Firearm objects. Useful for processing large result sets without managing pagination manually.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `sort` | string | no | Sort field |
| `order` | string | no | Sort order |
| `fields` | string | no | Comma-separated list of fields to include |
| `manufacturer` | string | no | Filter by manufacturer slug |
| `caliber` | string | no | Filter by caliber slug |
| `category` | string | no | Filter by category slug |
| `action_type` | string | no | Filter by action type, as stored and underscored. The vocabulary is /v1/firearms/action-types |
| `country_of_origin` | string | no | Filter by ISO 3166-1 alpha-2 country code |
| `year_introduced_min` | integer | no | Minimum year introduced |
| `year_introduced_max` | integer | no | Maximum year introduced |
| `weight_min` | number | no | Minimum empty weight (grams) |
| `weight_max` | number | no | Maximum empty weight (grams) |
| `barrel_length_min` | number | no | Minimum barrel length (mm) |
| `status` | string | no | Filter by production status |
| `features` | string | no | Comma-separated feature slugs; all must match |
| `has_3d_model` | string | no | Only firearms with (or without) a 3D model |
| `has_image` | string | no | Only firearms with (or without) real imagery: a render or a photograph, ignoring the line-art silhouette every firearm has |
| `created_after` | string | no | Only records added after this ISO 8601 timestamp |
| `created_before` | string | no | Only records added before this ISO 8601 timestamp |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmListItem[] | One page of results |
| `data[].id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. Stable: it is what a mirror keys on. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].status` | FirearmStatus \| null | 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. |
| `data[].countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data[].actionType` | string \| null | How the action cycles: `short_recoil`, `gas_operated`, `blowback`, `bolt_action` and so on. An open vocabulary: the values in use are published on the schema as `x-gunspec-vocabulary`, and `GET /v1/firearms/action-types` is the live list. |
| `data[].weightEmptyG` | number \| null | Unloaded weight in grams, without magazine. |
| `data[].barrelLengthMm` | number \| null | Barrel length in millimetres, measured from the breech face. |
| `data[].images` | InlineMediaItem[] | Every image this firearm has, silhouette first. Empty when none are on file. |
| `data[].images[].id` | integer | Autoincrementing image id. |
| `data[].images[].url` | string | Absolute, directly fetchable URL. |
| `data[].images[].kind` | MediaKind | What kind of asset a media row is. |
| `data[].images[].alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data[].images[].width` | integer \| null | Pixel width, where it is known. |
| `data[].images[].height` | integer \| null | Pixel height, where it is known. |
| `data[].createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data[].svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data[].model3dUrl` | string \| null | GLB model, or null where none is on file. |
| `data[].favoriteCount` | integer | How many accounts have favorited this record. |
| `data[].updatedAt` | string | When a field we serve last changed. Maintained by the database rather than by whatever wrote the row, so it moves for a corrected specification and does not move for a re-run of the importer. |
| `data[].version` | string \| null | Opaque fingerprint of this record. It changes when and only when a field we serve changes, so a client holding the same value already has the record and need not fetch it. Equality is the only supported operation: do not parse it, and do not assume how it is computed. Null means the record has not been written since versioning began, not that it is unchanged. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "glock-g17-gen5",
      "name": "Glock 17 Gen5",
      "manufacturerId": "glock",
      "categoryId": "pistol",
      "yearIntroduced": 2017,
      "status": "in_production",
      "countryOfOrigin": "AT",
      "actionType": "short_recoil",
      "weightEmptyG": 625,
      "barrelLengthMm": 114,
      "images": [
        {
          "id": 2572,
          "url": "https://api.gunspec.io/v1/firearms/ak-47/images/2572",
          "kind": "silhouette",
          "alt": "AK-47 right profile",
          "width": 1536,
          "height": 1024
        }
      ],
      "createdAt": "2026-09-06 23:55:07",
      "svgLineArtUrl": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/media/silhouette",
      "model3dUrl": null,
      "favoriteCount": 0,
      "updatedAt": "2026-09-10 06:42:19",
      "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- Returns an async iterator: it fetches each page as you consume it, so a loop over the whole collection is one statement and one page in memory at a time.

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms?category=pistol' \
  --header 'X-API-Key: your_key'
```

## client.firearms.search()

`GET /v1/firearms/search`

Auth: API key required  
Tier: Builder+

Full-text search across firearms.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | yes | Search query |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `manufacturer` | string | no | Only matches from this manufacturer slug |
| `category` | string | no | Only matches in this category slug |
| `action_type` | string | no | Only matches with this action type, as stored and underscored. The vocabulary is /v1/firearms/action-types |
| `country_of_origin` | string | no | Only matches from this ISO 3166-1 alpha-2 country code |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmSearchResult[] | One search hit. **The field names are snake_case here**, unlike `/v1/firearms` and the rest of the catalog: these are the search view's own columns and renaming them would break every consumer already reading them. `manufacturer_name` and `category_name` are joined in so a result list renders without a second call. |
| `data[].id` | string | URL-safe slug identifying the firearm. Stable: it is what a mirror keys on. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturer_id` | string \| null | Slug of the manufacturer. `manufacturerId` on the camelCase endpoints. |
| `data[].category_id` | string \| null | Slug of the category. `categoryId` on the camelCase endpoints. |
| `data[].manufacturer_name` | string \| null | Display name of the manufacturer, joined in so a result row needs no second call. |
| `data[].category_name` | string \| null | Display name of the category. |
| `data[].category_slug` | string \| null | Slug of the category again, under the name the search view uses. Same value as `category_id`. |
| `data[].year_introduced` | integer \| null | Year the model was introduced. |
| `data[].status` | FirearmStatus \| null | 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. |
| `data[].country_of_origin` | string \| null | ISO 3166-1 alpha-2 code of the country of origin. |
| `data[].action_type` | string \| null | Operating action as stored, underscored, e.g. `short_recoil`, `bolt_action`, `gas_operated`. The full vocabulary is `/v1/firearms/action-types`; a hyphenated value matches nothing. |
| `data[].weight_empty_g` | number \| null | Unloaded weight in grams, without magazine. |
| `data[].barrel_length_mm` | number \| null | Barrel length in millimetres, measured from the breech face. |
| `data[].svg_line_art_url` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data[].model_3d_url` | string \| null | GLB model, or null where none is on file. |
| `data[].favorite_count` | integer | How many accounts have favorited this record. |
| `data[].rank` | number \| null | FTS5 relevance score the page is ordered by. Lower is a better match (SQLite bm25 scores are negative), and it is only comparable within one response. Absent or null when the query fell back to a plain substring match, which has no ranking. |
| `data[].images` | InlineMediaItem[] | Every image this firearm has, silhouette first. Empty when none are on file. |
| `data[].images[].id` | integer | Autoincrementing image id. |
| `data[].images[].url` | string | Absolute, directly fetchable URL. |
| `data[].images[].kind` | MediaKind | What kind of asset a media row is. |
| `data[].images[].alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data[].images[].width` | integer \| null | Pixel width, where it is known. |
| `data[].images[].height` | integer \| null | Pixel height, where it is known. |
| `data[].matched_name` | string \| null | The firearm's alternate name the query matched, when its own name did not: a special edition is filed as an alternate name on the model it is an edition of, so searching for the edition returns the model with the edition's name here. Null when the record's own name matched, or the match came from the maker, a caliber or the description. |
| `data[].matched_name_kind` | string \| null | `edition` when `matched_name` is an edition of this model (a limited, anniversary, engraved or finish edition), `alternate` when it is another name for it. Null when `matched_name` is null. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "glock-g17-gen5",
      "name": "Glock 17 Gen5",
      "manufacturer_id": "glock",
      "category_id": "pistol",
      "manufacturer_name": "Glock Ges.m.b.H.",
      "category_name": "Pistol",
      "category_slug": "pistol",
      "year_introduced": 2017,
      "status": "in_production",
      "country_of_origin": "AT",
      "action_type": "short_recoil",
      "weight_empty_g": 625,
      "barrel_length_mm": 114,
      "svg_line_art_url": null,
      "model_3d_url": null,
      "favorite_count": 0,
      "rank": -3.42,
      "images": [
        {
          "id": 2572,
          "url": "https://api.gunspec.io/v1/firearms/ak-47/images/2572",
          "kind": "silhouette",
          "alt": "AK-47 right profile",
          "width": 1536,
          "height": 1024
        }
      ],
      "matched_name": "92FS Diamond",
      "matched_name_kind": "edition"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/search?q=glock' \
  --header 'X-API-Key: your_key'
```

## client.firearms.compare()

`GET /v1/firearms/compare`

Auth: API key required  
Tier: Builder+

Compare up to 5 firearms side by side.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | string | yes | Comma-separated firearm slugs, maximum 5 |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.items` | FirearmWithRelations[] | The compared firearms, in the order their ids were given. |
| `data.items[].has3dModel` | integer | Integer flag, `1` when a 3D model is on file. Stored as the database holds it rather than as a boolean. |
| `data.items[].svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data.items[].model3dUrl` | string \| null | GLB model, or null where none is on file. |
| `data.items[].defaultAmmoId` | string \| null | The ammunition load ballistics figures are quoted against, where one is nominated. |
| `data.items[].sourceMuzzleVelocityMps` | number \| null | Muzzle velocity in metres per second **exactly as the source stated it**, kept beside our own figure so a reader can see what was quoted and what was derived. |
| `data.items[].sourceMuzzleEnergyJ` | number \| null | Muzzle energy in joules exactly as the source stated it. |
| `data.items[].sourceEffectiveRangeM` | number \| null | Effective range in metres exactly as the source stated it. |
| `data.items[].sourceMaxRangeM` | number \| null | Maximum range in metres exactly as the source stated it. |
| `data.items[].ballisticsSource` | string \| null | Where the ballistics figures came from, named in prose. |
| `data.items[].ballisticsSourceUrl` | string \| null | URL of the ballistics source, where it is a page rather than a book. |
| `data.items[].id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. Stable: it is what a mirror keys on. |
| `data.items[].name` | string | Display name, as the maker writes it. |
| `data.deltas` | object[] | Per-field differences across the compared firearms, one entry per numeric field. |
| `data.deltas[].field` | string | Which column the row is about, named as the database names it. |
| `data.deltas[].values` | number \| null[] | This field on each compared firearm, in the order the ids were given. Null where that record does not carry the field. |
| `data.deltas[].min` | number \| null | Lowest value across the compared firearms. Null when fewer than two of them carry the field. |
| `data.deltas[].max` | number \| null | Highest value across the compared firearms. Null when fewer than two of them carry the field. |
| `data.deltas[].percentDiff` | number \| null | Spread from the lowest value, as a percentage of it: `(max - min) / min * 100`, rounded to two places. Null when the minimum is zero or fewer than two records carry the field. |

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "has3dModel": 0,
        "svgLineArtUrl": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/media/silhouette",
        "model3dUrl": null,
        "defaultAmmoId": "m882",
        "sourceMuzzleVelocityMps": 375,
        "sourceMuzzleEnergyJ": 565,
        "sourceEffectiveRangeM": 50,
        "sourceMaxRangeM": 1800,
        "ballisticsSource": "Manufacturer specification sheet",
        "ballisticsSourceUrl": "https://eu.glock.com/en/pistols/g17",
        "id": "glock-g17-gen5",
        "name": "Glock 17 Gen5",
        "manufacturerId": "glock",
        "categoryId": "pistol",
        "parentFirearmId": "glock-17",
        "variantType": "generation",
        "yearIntroduced": 2017,
        "yearDiscontinued": null,
        "status": "in_production",
        "countryOfOrigin": "AT",
        "weightEmptyG": 625,
        "weightLoadedG": 905,
        "overallLengthMm": 204,
        "barrelLengthMm": 114,
        "heightMm": 139,
        "widthMm": 34,
        "sightRadiusMm": 165,
        "actionType": "short_recoil",
        "firingMechanism": "striker-fired",
        "triggerType": "safe-action",
        "triggerPullN": 28,
        "magazineCapacity": 17,
        "magazineType": "detachable-box",
        "muzzleVelocityMps": 375,
        "muzzleEnergyJ": 570,
        "effectiveRangeM": 50,
        "maxRangeM": null,
        "rateOfFireRpm": null,
        "barrelRifling": "polygonal",
        "riflingTwistMm": 250,
        "numberOfGrooves": null,
        "frameMaterial": "polymer",
        "slideMaterial": "steel",
        "barrelMaterial": "steel",
        "stockMaterial": null,
        "finish": "nDLC",
        "safetyMechanisms": [
          "manual_safety_selector",
          "half_cock_notch"
        ],
        "features": [
          "collapsible_stock",
          "accessory_rail"
        ],
        "feedSystems": [
          "detachable_box_magazine"
        ],
        "gameDamage": null,
        "gameAccuracy": null,
        "gameRange": null,
        "gameFireRate": null,
        "gameMobility": null,
        "gameRecoilControl": null,
        "gameReloadSpeed": null,
        "gameConcealment": null,
        "description": null,
        "notes": null,
        "designer": "Mikhail Kalashnikov",
        "foldedLengthMm": 490,
        "alternateNames": [
          "Avtomat Kalashnikova",
          "Type 56",
          "Kalash"
        ],
        "firingModes": [
          "semi_automatic",
          "full_automatic"
        ],
        "conflicts": [
          {
            "name": "Vietnam War",
            "years": "1955-1975",
            "sides": [
              "North Vietnam / Viet Cong",
              "South Vietnam / United States"
            ]
          }
        ],
        "productionNumbers": {
          "estimated_total": 75000000,
          "production_years": "1947-1975",
          "notes": "Approximately 75 million AK-47 type rifles produced."
        },
        "lore": "The AK-47 is so iconic it appears on the national flag of Mozambique.",
        "sources": [
          "string"
        ],
        "verifiedAt": "2026-09-25",
        "verifiedFields": [
          "barrel_length_mm",
          "weight_empty_g"
        ],
        "dataConfidence": 0.95,
        "createdAt": "2025-01-15 12:00:00",
        "updatedAt": "2026-09-10 06:42:19",
        "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2",
        "provenance": {
          "sources": [
            "https://www.glock.com/en/products/pistols/g17-gen5"
          ],
          "sourceKinds": [
            {
              "url": "https://www.glock.com/en/products/pistols/g17-gen5",
              "kind": "manufacturer",
              "archivedFrom": null,
              "rank": 0,
              "tier": "authority",
              "basis": "declared",
              "evidence": null
            }
          ],
          "bestSourceKind": "manufacturer",
          "makerSource": "https://www.glock.com/en/products/pistols/g17-gen5",
          "evidence": "maker",
          "checks": {
            "raised": 3,
            "open": 0,
            "history": [
              {
                "task": "verify:firearms:glock-g17-gen5",
                "number": 1204,
                "check": "quality:firearm-weight-fits-its-size",
                "kind": "verify",
                "agent": "redback",
                "worker": "redback-046535",
                "status": "resolved",
                "decidedAt": "2026-09-25 07:42:39",
                "proposed": "Read https://us.glock.com/en/pistols/g17-gen5: agrees on barrel_length, weight.",
                "fields": [
                  "barrel_length_mm",
                  "weight_empty_g"
                ],
                "sources": [
                  "https://us.glock.com/en/pistols/g17-gen5"
                ],
                "review": {
                  "decision": "accepted",
                  "at": null
                },
                "finding": {
                  "why": "2.22 g/cm3 against a submachine-gun / carbon-steel median of 0.64; check the weight and the dimensions",
                  "evidence": {
                    "apparentDensity": 2.218,
                    "categoryMedian": 0.635
                  }
                }
              }
            ]
          },
          "dataConfidence": 0.95,
          "verifiedAt": "2026-09-08",
          "verifiedFields": [
            "barrel_length_mm",
            "weight_empty_g"
          ],
          "specSource": null,
          "updatedAt": "2026-09-08 04:10:22",
          "version": "ba5c1d9e06c279a5"
        },
        "manufacturer": {
          "id": "glock",
          "name": "Glock Ges.m.b.H.",
          "countryCode": "AT",
          "foundedYear": 1963,
          "status": "active",
          "defunctYear": null,
          "stateOwned": false,
          "makesFirearms": true,
          "parentId": null,
          "predecessorId": null,
          "website": "https://www.glock.com",
          "logoUrl": null,
          "description": null,
          "createdAt": "2025-01-15 12:00:00",
          "updatedAt": "2026-09-10 06:42:19",
          "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2"
        },
        "category": {
          "id": "pistol",
          "name": "Pistol",
          "description": "Handguns designed to be fired with one or two hands"
        },
        "calibers": [
          {
            "caliberId": "9x19mm-parabellum",
            "isPrimary": 1,
            "name": "9x19mm Parabellum",
            "natoDesignation": "9mm NATO",
            "bulletDiameterMm": 9.01,
            "caseLengthMm": 19.15,
            "cartridgeType": "rimless",
            "neckDiameterMm": 9.65,
            "shoulderDiameterMm": null,
            "baseDiameterMm": 9.93,
            "rimDiameterMm": 9.96,
            "rimThicknessMm": 1.27,
            "overallLengthMm": 29.69,
            "bulletLengthMm": 15.5,
            "typicalBulletWeightG": 7.45,
            "primerType": "small_pistol",
            "caseShape": "tapered",
            "caseMaterial": "brass",
            "projectileKind": "bullet",
            "bulletProfile": "round_nose",
            "closure": "bullet",
            "markingColor": null
          }
        ],
        "images": [
          {
            "id": 1,
            "firearmId": "glock-g17-gen5",
            "url": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/images/3076",
            "type": "svg",
            "source": "manufacturer",
            "license": "fair-use",
            "kind": "silhouette",
            "storage": "cdn",
            "author": null,
            "sourceUrl": null,
            "alt": null,
            "width": 1200,
            "height": 800,
            "sortOrder": 0
          }
        ],
        "users": [
          {
            "id": 1,
            "firearmId": "glock-g17-gen5",
            "userName": "Austrian Armed Forces",
            "userType": "military",
            "countryCode": "AT",
            "adoptedYear": 1982,
            "designation": "Pistole 80"
          }
        ]
      }
    ],
    "deltas": [
      {
        "field": "weightEmptyG",
        "values": [
          625
        ],
        "min": 625,
        "max": 945,
        "percentDiff": 51.2
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/compare?ids=glock-g17-gen5,sig-sauer-p320-full-size' \
  --header 'X-API-Key: your_key'
```

## client.firearms.loadCarriage()

`GET /v1/firearms/load-carriage`

Auth: API key required  
Tier: Builder+

Compare the metabolic cost of a foot march carrying each of up to 5 firearms. Builder. Each firearm is carried empty plus `magazines` full magazines and the named `attachments`, on top of `kit_kg` of other equipment, costed by the LCDA backpacking equation (Looney et al. 2022) or, with `model: 'pandolf'`, the Pandolf equation (1977). Masses come from the catalogue: a firearm missing its empty or loaded weight is answered with an `error` in place of its cost, and the others are still compared. Energy expenditure only: neither model predicts fatigue, marksmanship or time to exhaustion.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | string | yes | Comma-separated firearm slugs, maximum 5. |
| `body_mass_kg` | number | no | Soldier body mass in kg. |
| `body_fat_pct` | number | no | Body fat as a percentage of body mass; the LCDA resting rate is computed from lean mass. |
| `kit_kg` | number | no | Everything else carried, in kg: armour, pack, water. |
| `magazines` | integer | no | Full magazines carried in total, the one in the firearm included. 0 carries it unloaded. |
| `attachments` | string | no | Comma-separated attachment slugs, maximum 10, added to every firearm by their recorded weight. |
| `speed_kmh` | number | no | Marching speed in km/h. |
| `grade_pct` | number | no | Slope as a percentage, rise over run: negative is downhill. |
| `terrain` | string | no | Surface: `paved` (paved road or treadmill), `dirt_road` (dirt road), `light_brush` (light brush), `heavy_brush` (heavy brush), `swampy_bog` (swampy bog), `loose_sand` (loose sand). |
| `distance_km` | number | no | March distance in km. |
| `model` | string | no | Equation: `lcda` (default) or `pandolf`, which refuses a downhill grade. |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.model` | object | The equation behind every figure in the answer. |
| `data.model.id` | string | The model the costs were computed with. |
| `data.model.name` | string | The model by name. |
| `data.model.equation` | string | The equation as written, with its units, so the figures can be reproduced. |
| `data.model.citations` | object[] | Where the equation and its constants are from. |
| `data.model.validated` | object \| null | The range the equation was fitted on. Null for a model whose paper states none here. |
| `data.march` | object | The march every firearm was costed on. |
| `data.march.bodyMassKg` | number | Soldier body mass in kg. |
| `data.march.bodyFatPct` | number | Body fat as a percentage of body mass. |
| `data.march.kitKg` | number | Other equipment carried, in kg. |
| `data.march.magazines` | integer | Full magazines carried in total. |
| `data.march.speedKmh` | number | Marching speed in km/h. |
| `data.march.speedMps` | number | Marching speed in m/s, as the equations read it. |
| `data.march.gradePct` | number | Slope as a percentage. |
| `data.march.terrain` | string | Surface id. |
| `data.march.terrainFactor` | number | The terrain factor (Soule and Goldman 1972) the moving cost is multiplied by. |
| `data.march.distanceKm` | number | March distance in km. |
| `data.march.durationS` | integer | How long the march takes at this speed, in seconds. |
| `data.march.defaulted` | string[] | The query parameters not sent, answered from their defaults. |
| `data.attachments` | object[] | The attachments added to every firearm. |
| `data.attachments[].id` | string | Attachment slug. |
| `data.attachments[].name` | string | Attachment name. |
| `data.attachments[].weightG` | number | Recorded weight in grams. |
| `data.results` | object[] | One result per firearm, in the order their ids were given. |
| `data.results[].firearm` | object | The firearm this result is for. |
| `data.results[].load` | object | What is carried, piece by piece. |
| `data.results[].cost` | object \| null | What the march costs this soldier with this firearm. Null when a mass it needs is not recorded; `error` says which. |
| `data.results[].delta` | object \| null | Against the option with the lightest carried load. Null on that option, on a lone firearm, and where no cost could be computed. |
| `data.results[].warnings` | string[] | Where an input lies outside the fitted range, or a count needs qualifying. |
| `data.results[].error` | string \| null | Why this firearm could not be costed. Null when it was. |

```json
{
  "success": true,
  "data": {
    "model": {
      "id": "lcda",
      "name": "LCDA backpacking equation with the LCDA graded-walking term",
      "equation": "string",
      "citations": [
        {
          "authors": "Looney DP, Lavoie EM, Vangala SV, Holden LD, Figueiredo PS, Friedl KE, et al.",
          "year": 2022,
          "title": "Modeling the Metabolic Costs of Heavy Military Backpacking",
          "journal": "Medicine & Science in Sports & Exercise 54(4):646-654",
          "doi": "10.1249/MSS.0000000000002833"
        }
      ],
      "validated": {
        "speedMps": [
          0.45
        ],
        "loadFraction": [
          0.66
        ]
      }
    },
    "march": {
      "bodyMassKg": 80,
      "bodyFatPct": 15,
      "kitKg": 25,
      "magazines": 7,
      "speedKmh": 4.8,
      "speedMps": 1.333,
      "gradePct": 0,
      "terrain": "paved",
      "terrainFactor": 1,
      "distanceKm": 20,
      "durationS": 15000,
      "defaulted": [
        "body_mass_kg"
      ]
    },
    "attachments": [
      {
        "id": "aimpoint-compm5",
        "name": "Aimpoint CompM5",
        "weightG": 148
      }
    ],
    "results": [
      {
        "firearm": {
          "id": "hk416",
          "name": "HK416",
          "magazineType": "detachable_box",
          "magazineCapacity": 30
        },
        "load": {
          "firearmEmptyG": 3020,
          "loadedMagazineG": 400,
          "magazines": 7,
          "magazinesG": 2800,
          "attachmentsG": 148,
          "carriedG": 5968,
          "totalLoadKg": 30.968,
          "loadFraction": 0.387,
          "sources": {
            "firearmEmptyG": "catalogue",
            "loadedMagazineG": "catalogue"
          }
        },
        "cost": {
          "watts": 481.2,
          "wattsPerKg": 6.02,
          "energyKj": 7218,
          "energyKcal": 1725.2
        },
        "delta": {
          "carriedG": 390,
          "watts": 6.1,
          "energyKj": 91.5,
          "energyKcal": 21.9
        },
        "warnings": [
          "string"
        ],
        "error": null
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/load-carriage?ids=hk416,fn-scar-l&kit_kg=25&magazines=7&distance_km=20' \
  --header 'X-API-Key: your_key'
```

## client.firearms.recoil()

`GET /v1/firearms/recoil`

Auth: API key required  
Tier: Builder+

Compare the free recoil of each of up to 5 firearms firing a load. Builder. By the free recoil formula SAAMI publishes: the bullet's momentum, and the powder gas's at SAAMI's gas velocity factor for the class of firearm, over the firearm's recorded mass. The muzzle velocity is the one computed for each firearm's own barrel. The catalogue holds no powder charge: without `powder_charge_g` only the bullet is counted and every figure is a lower bound (`recoil.lowerBound`). Free recoil, not felt recoil.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | string | yes | Comma-separated firearm slugs, maximum 5. |
| `ammo_id` | string | no | The load every firearm fires. Without it, each fires the load its ballistic profile uses. |
| `mass` | string | no | Which recorded weight the firearm recoils with: `loaded` or `empty`. |
| `powder_charge_g` | number | no | The load's powder charge in grams. Needs `ammo_id`. Without it the figures are a lower bound. |
| `gas_class` | string | no | The SAAMI class whose gas velocity factor applies, in place of the one read from each firearm's category: `rifle` (high powered rifles, 1.75), `shotgun` (shotguns (average length), 1.5), `shotgun_long_barrel` (shotguns (long barrel), 1.25), `handgun` (pistol & revolvers, 1.5). |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.method` | object | The formula behind every figure in the answer. |
| `data.method.id` | string | The method the figures were computed with. |
| `data.method.name` | string | The method by name. |
| `data.method.equation` | string | The equation as written, with its units, so the figures can be reproduced. |
| `data.method.citations` | object[] | Where the formula and its factors are from. |
| `data.method.gasFactors` | object | SAAMI's gas velocity factor for each class, as a multiple of the muzzle velocity. |
| `data.shot` | object | The shot every firearm was answered for. |
| `data.shot.mass` | string | Which recorded weight was asked for. |
| `data.shot.powderChargeG` | number \| null | The powder charge given, in grams. Null when none was. |
| `data.shot.gasClass` | string \| null | The class given in the query. Null when each firearm's class came from its category. |
| `data.shot.lowerBound` | boolean | True when no powder charge was given, so every figure is a lower bound. |
| `data.shot.defaulted` | string[] | The query parameters not sent, answered from their defaults. |
| `data.results` | object[] | One result per firearm, in the order their ids were given. |
| `data.results[].firearm` | object | The firearm this result is for. |
| `data.results[].ammunition` | object \| null | The load fired: the one named by `ammo_id`, or the one `/v1/firearms/{id}/load` fires this firearm with. Null when there is none. |
| `data.results[].mass` | object | The mass that recoils. |
| `data.results[].muzzleVelocityMps` | number \| null | The load's muzzle velocity from this firearm's barrel, in m/s. |
| `data.results[].gas` | object | The powder gas term. |
| `data.results[].recoil` | object \| null | The free recoil. Null when a figure it needs is not recorded; `error` says which. |
| `data.results[].delta` | object \| null | Against the option with the least recoil energy. Null on that option, on a lone firearm, and where no recoil was computed. |
| `data.results[].warnings` | string[] | What an answer assumed in place of a missing figure, or why the gas was not counted. |
| `data.results[].error` | string \| null | Why this firearm could not be answered. Null when it was. |

```json
{
  "success": true,
  "data": {
    "method": {
      "id": "saami-free-recoil",
      "name": "SAAMI free recoil",
      "equation": "string",
      "citations": [
        {
          "authors": "Sporting Arms and Ammunition Manufacturers' Institute (SAAMI)",
          "year": 2018,
          "title": "Gun Recoil, Technical: Free Recoil Energy",
          "journal": "SAAMI technical sheet, Rev. 7/9/2018",
          "doi": null,
          "url": "https://saami.org/wp-content/uploads/2025/03/Gun-Recoil-Formulae-2018-07-9.pdf"
        }
      ],
      "gasFactors": {
        "rifle": 1.75,
        "shotgun": 1.5,
        "shotgun_long_barrel": 1.25,
        "handgun": 1.5
      }
    },
    "shot": {
      "mass": "loaded",
      "powderChargeG": null,
      "gasClass": null,
      "lowerBound": true,
      "defaulted": [
        "mass"
      ]
    },
    "results": [
      {
        "firearm": {
          "id": "hk416",
          "name": "HK416",
          "categoryId": "assault-rifle",
          "barrelLengthMm": 264
        },
        "ammunition": {
          "id": "m855",
          "name": "M855 Ball (SS109)",
          "caliberId": "5-56x45mm-nato",
          "bulletWeightG": 4.02
        },
        "mass": {
          "firearmG": 3420,
          "basis": "loaded",
          "source": "catalogue"
        },
        "muzzleVelocityMps": 777,
        "gas": {
          "class": "rifle",
          "label": "High powered rifles",
          "factor": 1.75,
          "velocityMps": null,
          "chargeG": null,
          "classSource": "query"
        },
        "recoil": {
          "impulseNs": 3.125,
          "bulletImpulseNs": 3.125,
          "gasImpulseNs": null,
          "velocityMps": 0.914,
          "energyJ": 1.43,
          "lowerBound": true
        },
        "delta": {
          "energyJ": 0.36,
          "velocityMps": 0.123,
          "impulseNs": 0.316
        },
        "warnings": [
          "string"
        ],
        "error": null
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/recoil?ids=hk416,m4-carbine&ammo_id=m855' \
  --header 'X-API-Key: your_key'
```

## client.firearms.pointBlank()

`GET /v1/firearms/point-blank`

Auth: API key required  
Tier: Builder+

Compare the maximum point-blank range and supersonic range of up to 5 firearms. Builder. On the trajectory the ballistic profile flies (the same load, the firearm's own barrel, the same solver, the ICAO standard atmosphere): the furthest distance the bullet stays within half `target_mm` of the line of sight, the zero that gives it, and how far it stays supersonic.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | string | yes | Comma-separated firearm slugs, maximum 5. |
| `ammo_id` | string | no | The load every firearm fires. Without it, each fires the load its ballistic profile uses. |
| `target_mm` | number | no | The target's diameter in mm: the bullet must stay within half of it above and below the line of sight. |
| `sight_height_mm` | number | no | Height of the sights above the bore in mm. Default: the height the ballistic profile assumes for the firearm's category. |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.method` | object | The method behind every figure in the answer. |
| `data.method.id` | string | The method the figures were computed with. |
| `data.method.name` | string | The method by name. |
| `data.method.equation` | string | The geometry as written, with its units, so the figures can be reproduced. |
| `data.method.citations` | object[] | Where the atmosphere the trajectory is flown through is defined. |
| `data.method.resolutionM` | integer | The step, in metres, every distance is found to. |
| `data.method.maxRangeM` | integer | How far each trajectory is flown, in metres. |
| `data.atmosphere` | object | The atmosphere, as the trajectory solver uses it. |
| `data.atmosphere.name` | string | The air every trajectory is flown through. |
| `data.atmosphere.temperatureC` | number | Air temperature in °C. |
| `data.atmosphere.pressureKpa` | number | Air pressure in kPa. |
| `data.atmosphere.airDensityKgM3` | number | Air density in kg/m³. |
| `data.atmosphere.speedOfSoundMps` | number | Speed of sound in m/s: the line the supersonic range is drawn at. |
| `data.target` | object | The target every firearm was answered for. |
| `data.target.diameterMm` | number | Target diameter in mm. |
| `data.target.radiusMm` | number | Half of it: how far above or below the line of sight the bullet may be. |
| `data.target.defaulted` | string[] | The query parameters not sent, answered from their defaults. |
| `data.results` | object[] | One result per firearm, in the order their ids were given. |
| `data.results[].firearm` | object | The firearm this result is for. |
| `data.results[].ammunition` | object \| null | The load fired: the one named by `ammo_id`, or the one `/v1/firearms/{id}/load` fires this firearm with. Null when there is none. |
| `data.results[].muzzleVelocityMps` | number \| null | The load's muzzle velocity from this firearm's barrel, in m/s. |
| `data.results[].sight` | object | The sights the path is measured from. |
| `data.results[].pointBlank` | object \| null | The maximum point-blank range. Null when it could not be computed; `error` says why. |
| `data.results[].supersonic` | object \| null | How far the bullet stays supersonic. Null when no trajectory was flown. |
| `data.results[].delta` | object \| null | Against the option with the longest point-blank range. Null on that option, on a lone firearm, and where no range was computed. |
| `data.results[].warnings` | string[] | What an answer needs qualifying with. |
| `data.results[].error` | string \| null | Why this firearm could not be answered. Null when it was. |

```json
{
  "success": true,
  "data": {
    "method": {
      "id": "maximum-point-blank-range",
      "name": "Maximum point-blank range on the API trajectory",
      "equation": "string",
      "citations": [
        {
          "authors": "Sporting Arms and Ammunition Manufacturers' Institute (SAAMI)",
          "year": 2018,
          "title": "Gun Recoil, Technical: Free Recoil Energy",
          "journal": "SAAMI technical sheet, Rev. 7/9/2018",
          "doi": null,
          "url": "https://saami.org/wp-content/uploads/2025/03/Gun-Recoil-Formulae-2018-07-9.pdf"
        }
      ],
      "resolutionM": 1,
      "maxRangeM": 2500
    },
    "atmosphere": {
      "name": "ICAO standard atmosphere at sea level",
      "temperatureC": 15,
      "pressureKpa": 101.325,
      "airDensityKgM3": 1.225,
      "speedOfSoundMps": 340.294
    },
    "target": {
      "diameterMm": 200,
      "radiusMm": 100,
      "defaulted": [
        "sight_height_mm"
      ]
    },
    "results": [
      {
        "firearm": {
          "id": "hk416",
          "name": "HK416",
          "categoryId": "assault-rifle",
          "barrelLengthMm": 264
        },
        "ammunition": {
          "id": "m855",
          "name": "M855 Ball (SS109)",
          "caliberId": "5-56x45mm-nato",
          "bulletWeightG": 4.02
        },
        "muzzleVelocityMps": 825,
        "sight": {
          "heightMm": 38,
          "source": "query"
        },
        "pointBlank": {
          "rangeM": 281,
          "nearZeroM": 21,
          "farZeroM": 238,
          "peakM": 138,
          "peakHeightMm": 100,
          "boreAngleMrad": 2.043,
          "timeOfFlightS": 0.36,
          "velocityMps": 738
        },
        "supersonic": {
          "rangeM": 1105,
          "timeOfFlightS": 1.856,
          "beyondProfile": false
        },
        "delta": {
          "rangeM": -7,
          "farZeroM": -6
        },
        "warnings": [
          "string"
        ],
        "error": null
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/point-blank?ids=hk416,fn-scar-h&target_mm=200' \
  --header 'X-API-Key: your_key'
```

## client.firearms.ammoLoad()

`GET /v1/firearms/ammo-load`

Auth: API key required  
Tier: Builder+

Compare how much ammunition each of up to 5 firearms carries per kilogram. Builder. Derived figures (a full magazine as loaded less empty weight, rounds per kilogram, what `budget_kg` holds) and estimated ones (a basic load's mass by the fitted cartridge-mass model, with its error as a range) are labelled and never mixed. A derived magazine lighter than its rounds is reported with a warning.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | string | yes | Comma-separated firearm slugs, maximum 5. |
| `ammo_id` | string | no | A load whose bullet the estimate uses, for the firearms that fire its cartridge. Without it, each cartridge's typical bullet. |
| `budget_kg` | number | no | A weight of full magazines to fill, in kg. |
| `magazines` | integer | no | Full magazines in the estimated basic load. |
| `rounds` | integer | no | Rounds in the estimated basic load. Default: `magazines` times the capacity. |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.method` | object | The method behind every figure in the answer. |
| `data.method.id` | string | The method the figures were computed with. |
| `data.method.name` | string | The method by name. |
| `data.method.equation` | string | The arithmetic and the model as written, with units, so the figures can be reproduced. |
| `data.method.maxErrorPct` | number | The model's largest error on its reference cartridges, in percent: the range every estimate carries. |
| `data.method.fittedEnvelopeCm3` | number[] | The smallest and largest case envelope the model was fitted on, in cm³. |
| `data.method.references` | object[] | The cartridges the model was fitted to. |
| `data.load` | object | The load every firearm was answered for. |
| `data.load.budgetKg` | number | The weight budget in kg. |
| `data.load.magazines` | integer | Full magazines in the estimated basic load. |
| `data.load.rounds` | integer \| null | Rounds in the estimated basic load, as given. Null when each firearm's came from its capacity. |
| `data.load.ammoId` | string \| null | The load named, if any. |
| `data.load.defaulted` | string[] | The query parameters not sent, answered from their defaults. |
| `data.results` | object[] | One result per firearm, in the order their ids were given. |
| `data.results[].firearm` | object | The firearm this result is for. |
| `data.results[].cartridge` | object \| null | The cartridge the estimate is for. Null when the firearm records none. |
| `data.results[].magazine` | object \| null | A full magazine, derived. Null when the feed is not detachable or a weight or the capacity is not recorded. |
| `data.results[].budget` | object \| null | What `budget_kg` of full magazines holds. Null when no full magazine was derived. |
| `data.results[].basicLoad` | object \| null | A basic load's mass by the cartridge-mass model. Null when the cartridge lacks the figures the model needs. |
| `data.results[].delta` | object \| null | Against the option that carries the most rounds per kilogram, leaving out any magazine the model finds lighter than its rounds. Null on that option, on a lone firearm, and where no magazine was derived. |
| `data.results[].warnings` | string[] | Why a figure is missing, or why a derived one is doubtful. |
| `data.results[].error` | string \| null | Why nothing could be computed for this firearm. Null when anything was. |

```json
{
  "success": true,
  "data": {
    "method": {
      "id": "ammo-load",
      "name": "Magazine arithmetic and the fitted cartridge-mass model",
      "equation": "string",
      "maxErrorPct": 8.5,
      "fittedEnvelopeCm3": [
        2.6
      ],
      "references": [
        {
          "name": "7.62×39mm",
          "load": "57-N-231",
          "roundG": 16.3,
          "source": "https://en.wikipedia.org/wiki/7.62%C3%9739mm"
        }
      ]
    },
    "load": {
      "budgetKg": 5,
      "magazines": 7,
      "rounds": null,
      "ammoId": null,
      "defaulted": [
        "magazines"
      ]
    },
    "results": [
      {
        "firearm": {
          "id": "hk416",
          "name": "HK416",
          "magazineType": "detachable_box",
          "magazineCapacity": 30
        },
        "cartridge": {
          "caliberId": "5-56x45mm-nato",
          "bulletG": 4,
          "bulletSource": "caliber",
          "ammunitionId": null,
          "envelopeCm3": 3.222
        },
        "magazine": {
          "loadedMagazineG": 400,
          "capacity": 30,
          "roundsPerKg": 75,
          "source": "derived",
          "check": {
            "verdict": "plausible",
            "roundsG": {
              "expected": 361.7,
              "low": 1,
              "high": 1
            },
            "bandG": {
              "low": 309,
              "high": 879.3
            },
            "source": "estimated"
          }
        },
        "budget": {
          "magazines": 12,
          "rounds": 360,
          "massG": 4800,
          "leftoverG": 200,
          "source": "derived"
        },
        "basicLoad": {
          "rounds": 210,
          "roundG": {
            "expected": 12.06,
            "low": 1,
            "high": 1
          },
          "roundsG": {
            "expected": 2531.6,
            "low": 1,
            "high": 1
          },
          "magazines": 7,
          "magazinesG": {
            "expected": 3345.3,
            "low": 1,
            "high": 1
          },
          "source": "estimated"
        },
        "delta": {
          "roundsPerKg": 25,
          "budgetRounds": 120
        },
        "warnings": [
          "string"
        ],
        "error": null
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/ammo-load?ids=hk416,ak-74&budget_kg=5' \
  --header 'X-API-Key: your_key'
```

## client.firearms.gameMeta()

`GET /v1/firearms/game-meta`

Auth: API key required  
Tier: Builder+

Retrieve game metadata for firearms (archetypes, stat ranges, etc.).

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `archetype` | string | no | Restrict to a single archetype |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | GameProfile[] | The response payload. |
| `data[].id` | string | Slug of the firearm this profile is for. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].gameDamage` | integer \| null | Editorial game statistic, 0-100. **Not a measured figure.** These are balance numbers for game use and are not derived from the ballistics above. |
| `data[].gameAccuracy` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data[].gameRange` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data[].gameFireRate` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data[].gameMobility` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data[].gameRecoilControl` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data[].gameReloadSpeed` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data[].gameConcealment` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data[].archetype` | GameArchetype | The role a firearm's game profile assigns it. |
| `data[].strengths` | string[] | The statistics this firearm scores highest on. |
| `data[].weaknesses` | string[] | The statistics it scores lowest on. |

```json
{
  "success": true,
  "data": [
    {
      "id": "glock-g17-gen5",
      "name": "Glock 17 Gen5",
      "gameDamage": 45,
      "gameAccuracy": 70,
      "gameRange": 35,
      "gameFireRate": 55,
      "gameMobility": 85,
      "gameRecoilControl": 65,
      "gameReloadSpeed": 75,
      "gameConcealment": 80,
      "archetype": "sniper",
      "strengths": [
        "mobility",
        "concealment"
      ],
      "weaknesses": [
        "range"
      ]
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/game-meta' \
  --header 'X-API-Key: your_key'
```

## client.firearms.actionTypes()

`GET /v1/firearms/action-types`

Auth: API key required  
Tier: Explorer+

List all known action types across the database.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | The response payload. |
| `data[].id` | string | The action type as stored, and the value to send as `action_type` when filtering. |
| `data[].name` | string | Display name, as the maker writes it. |

```json
{
  "success": true,
  "data": [
    {
      "id": "blow_forward",
      "name": "Blow Forward"
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/action-types' \
  --header 'X-API-Key: your_key'
```

## client.firearms.filterOptions()

`GET /v1/firearms/filter-options`

Auth: API key required  
Tier: Explorer+

Get available filter options for the firearms list endpoint. Returns distinct values for manufacturers, calibers, categories, action types, countries, and statuses that can be used as filter values.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The distinct filter values currently present in the catalog. |
| `data.manufacturers` | object[] | Manufacturers with at least one firearm. |
| `data.manufacturers[].id` | string | Slug of the manufacturer, and the value to send as `manufacturer`. |
| `data.manufacturers[].name` | string | Display name, as the maker writes it. |
| `data.manufacturers[].countryCode` | string \| null | ISO 3166-1 alpha-2 code of where they are based, for grouping the list by country. |
| `data.categories` | object[] | Categories with at least one firearm. |
| `data.categories[].slug` | string | Slug of the category, and the value to send as `category`. Keyed `slug` here rather than `id`, unlike the other lists. |
| `data.categories[].name` | string | Display name, as the maker writes it. |
| `data.calibers` | object[] | Cartridges chambered by at least one firearm. |
| `data.calibers[].id` | string | Slug of the cartridge, and the value to send as `caliber`. |
| `data.calibers[].name` | string | Display name, as the maker writes it. |
| `data.actionTypes` | object[] | Operating actions present in the catalog. |
| `data.actionTypes[].id` | string | The action type as stored, and the value to send as `action_type`. |
| `data.actionTypes[].name` | string | Display name, as the maker writes it. |
| `data.features` | object[] | JSON array of notable features, as a string. The same vocabulary `/v1/firearms/by-feature` filters on. |
| `data.features[].id` | string | The feature as stored, and the value to send as `feature` to /v1/firearms/by-feature. |
| `data.features[].name` | string | Display name, as the maker writes it. |

```json
{
  "success": true,
  "data": {
    "manufacturers": [
      {
        "id": "glock",
        "name": "Glock",
        "countryCode": "AT"
      }
    ],
    "categories": [
      {
        "slug": "pistol",
        "name": "Pistol"
      }
    ],
    "calibers": [
      {
        "id": "9x19mm-parabellum",
        "name": "9x19mm Parabellum"
      }
    ],
    "actionTypes": [
      {
        "id": "semi_automatic",
        "name": "Semi Automatic"
      }
    ],
    "features": [
      {
        "id": "threaded barrel",
        "name": "Threaded Barrel"
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/filter-options' \
  --header 'X-API-Key: your_key'
```

## client.firearms.random()

`GET /v1/firearms/random`

Auth: API key required  
Tier: Explorer+

Get a random firearm, optionally filtered by category or country.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `category` | string | no | Restrict the draw to a category slug |
| `country` | string | no | Restrict the draw to an ISO country code |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.id` | string | Slug of the drawn firearm. |
| `data.name` | string | Display name, as the maker writes it. |
| `data.manufacturer_id` | string \| null | Slug of the manufacturer. Null where the maker is unrecorded. |
| `data.category_id` | string \| null | Slug of the category the firearm belongs to. |
| `data.year_introduced` | integer \| null | Year the model was introduced. |
| `data.status` | string \| null | 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. |
| `data.country_of_origin` | string \| null | ISO 3166-1 alpha-2 code of the country of origin. |
| `data.action_type` | string \| null | Operating action as stored, underscored, e.g. `short_recoil`, `bolt_action`, `gas_operated`. The full vocabulary is `/v1/firearms/action-types`; a hyphenated value matches nothing. |
| `data.weight_empty_g` | number \| null | Unloaded weight in grams. |
| `data.barrel_length_mm` | number \| null | Barrel length in millimetres. |

```json
{
  "success": true,
  "data": {
    "id": "glock-g17-gen5",
    "name": "Glock 17 Gen5",
    "manufacturer_id": "glock",
    "category_id": "pistol",
    "year_introduced": 2017,
    "status": "in_production",
    "country_of_origin": "AT",
    "action_type": "short_recoil",
    "weight_empty_g": 625,
    "barrel_length_mm": 114
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/random?category=pistol' \
  --header 'X-API-Key: your_key'
```

## client.firearms.top()

`GET /v1/firearms/top`

Auth: API key required  
Tier: Builder+

Get top firearms ranked by a specific statistic.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `stat` | string | yes | Which superlative to rank by |
| `category` | string | no | Restrict the ranking to a category slug |
| `limit` | integer | no | How many to return |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | TopFirearm[] | The response payload. |
| `data[].id` | string | Slug of the firearm. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].value` | number \| null | The figure this ranking is by, in the unit of whichever statistic was requested. |
| `data[].unit` | string | One of g, m, rpm, mm, rounds, J |

```json
{
  "success": true,
  "data": [
    {
      "id": "glock-g17-gen5",
      "name": "Glock 17 Gen5",
      "manufacturerId": "glock",
      "categoryId": "pistol",
      "value": 625,
      "unit": "g"
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/top?stat=lightest&category=pistol&limit=10' \
  --header 'X-API-Key: your_key'
```

## client.firearms.headToHead()

`GET /v1/firearms/head-to-head`

Auth: API key required  
Tier: Builder+

Compare two firearms in a head-to-head matchup.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `a` | string | yes | First firearm slug |
| `b` | string | yes | Second firearm slug |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.a` | HeadToHeadFirearm | One of the two compared firearms, as selected: the sixteen columns the verdicts are computed from, **snake_case**, under their database names. |
| `data.a.id` | string | URL-safe slug identifying the firearm. Stable: it is what a mirror keys on. |
| `data.a.name` | string | Display name, as the maker writes it. |
| `data.a.manufacturer_id` | string \| null | Slug of the manufacturer. `manufacturerId` on the camelCase endpoints. |
| `data.a.category_id` | string \| null | Slug of the category. `categoryId` on the camelCase endpoints. |
| `data.a.weight_empty_g` | number \| null | Unloaded weight in grams, without magazine. |
| `data.a.overall_length_mm` | number \| null | Overall length in millimetres. |
| `data.a.barrel_length_mm` | number \| null | Barrel length in millimetres, measured from the breech face. |
| `data.a.magazine_capacity` | integer \| null | Standard magazine capacity, in rounds. |
| `data.a.muzzle_velocity_mps` | number \| null | Muzzle velocity in metres per second, with the reference load. |
| `data.a.muzzle_energy_j` | number \| null | Muzzle energy in joules, with the reference load. |
| `data.a.effective_range_m` | number \| null | Effective range in metres. |
| `data.a.rate_of_fire_rpm` | number \| null | Cyclic rate of fire in rounds per minute; null for a semi-automatic or manual action. |
| `data.b` | HeadToHeadFirearm | One of the two compared firearms, as selected: the sixteen columns the verdicts are computed from, **snake_case**, under their database names. |
| `data.b.id` | string | URL-safe slug identifying the firearm. Stable: it is what a mirror keys on. |
| `data.b.name` | string | Display name, as the maker writes it. |
| `data.b.manufacturer_id` | string \| null | Slug of the manufacturer. `manufacturerId` on the camelCase endpoints. |
| `data.b.category_id` | string \| null | Slug of the category. `categoryId` on the camelCase endpoints. |
| `data.b.weight_empty_g` | number \| null | Unloaded weight in grams, without magazine. |
| `data.b.overall_length_mm` | number \| null | Overall length in millimetres. |
| `data.b.barrel_length_mm` | number \| null | Barrel length in millimetres, measured from the breech face. |
| `data.b.magazine_capacity` | integer \| null | Standard magazine capacity, in rounds. |
| `data.b.muzzle_velocity_mps` | number \| null | Muzzle velocity in metres per second, with the reference load. |
| `data.b.muzzle_energy_j` | number \| null | Muzzle energy in joules, with the reference load. |
| `data.b.effective_range_m` | number \| null | Effective range in metres. |
| `data.b.rate_of_fire_rpm` | number \| null | Cyclic rate of fire in rounds per minute; null for a semi-automatic or manual action. |
| `data.verdicts` | object | One verdict per compared stat, keyed by the stat's camelCase name (`weightEmptyG`, `overallLengthMm`, `barrelLengthMm`, `magazineCapacity`, `muzzleVelocityMps`, `muzzleEnergyJ`, `effectiveRangeM`, `rateOfFireRpm`). A stat neither firearm carries is omitted, so the key set is the stats that could be compared. |

```json
{
  "success": true,
  "data": {
    "a": {
      "id": "glock-g17-gen5",
      "name": "Glock 17 Gen5",
      "manufacturer_id": "glock",
      "category_id": "pistol",
      "weight_empty_g": 625,
      "overall_length_mm": 202,
      "barrel_length_mm": 114,
      "magazine_capacity": 17,
      "muzzle_velocity_mps": 375,
      "muzzle_energy_j": 500,
      "effective_range_m": 50,
      "rate_of_fire_rpm": null,
      "year_introduced": 2017,
      "status": "in_production",
      "country_of_origin": "AT",
      "action_type": "short_recoil"
    },
    "b": {
      "id": "glock-g17-gen5",
      "name": "Glock 17 Gen5",
      "manufacturer_id": "glock",
      "category_id": "pistol",
      "weight_empty_g": 625,
      "overall_length_mm": 202,
      "barrel_length_mm": 114,
      "magazine_capacity": 17,
      "muzzle_velocity_mps": 375,
      "muzzle_energy_j": 500,
      "effective_range_m": 50,
      "rate_of_fire_rpm": null,
      "year_introduced": 2017,
      "status": "in_production",
      "country_of_origin": "AT",
      "action_type": "short_recoil"
    },
    "verdicts": {
      "weightEmptyG": {
        "winner": "a",
        "a": 625,
        "b": 833,
        "better": "lighter"
      },
      "magazineCapacity": {
        "winner": "b",
        "a": 17,
        "b": 21,
        "better": "higher"
      }
    }
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/head-to-head?a=glock-g17-gen5&b=sig-sauer-p320-full-size' \
  --header 'X-API-Key: your_key'
```

## client.firearms.byFeature()

`GET /v1/firearms/by-feature`

Auth: API key required  
Tier: Builder+

Filter firearms by a specific feature.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `feature` | string | yes | Feature name as stored, e.g. `threaded barrel`. The vocabulary is `features` on /v1/firearms/filter-options |
| `category` | string | no | Restrict to a category slug |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | One page of results |
| `data[].id` | string | Slug of the firearm. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].features` | string[] | Every feature this firearm carries, not only the one filtered on. |
| `data[].featureIcons` | string \| null[] | The drawn icon each of `features` borrows, as its URL on the assets CDN (a WebP that loads without a key), in the same order, or null for a feature no drawing fits. Null where `features` is. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "m16a1",
      "name": "M16A1",
      "manufacturerId": "colt",
      "categoryId": "rifle",
      "features": [
        "threaded_barrel",
        "accessory_rail"
      ],
      "featureIcons": [
        "https://assets.gunspec.io/content/features/simple/threaded_barrel.webp",
        "https://assets.gunspec.io/content/features/simple/accessory_rail.webp"
      ]
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/by-feature?feature=threaded%20barrel&category=pistol' \
  --header 'X-API-Key: your_key'
```

## client.firearms.byAction()

`GET /v1/firearms/by-action`

Auth: API key required  
Tier: Explorer+

Filter firearms by action type.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `action` | string | yes | Action type as stored, underscored, e.g. `short_recoil`. The vocabulary is /v1/firearms/action-types |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | One page of results |
| `data[].id` | string | Slug of the firearm. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].status` | string \| null | 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. |
| `data[].actionType` | string \| null | The action this firearm uses, as stored. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "m16a1",
      "name": "M16A1",
      "manufacturerId": "colt",
      "categoryId": "rifle",
      "yearIntroduced": 1967,
      "status": "in_production",
      "actionType": "short_recoil"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/by-action?action=short_recoil' \
  --header 'X-API-Key: your_key'
```

## client.firearms.byMaterial()

`GET /v1/firearms/by-material`

Auth: API key required  
Tier: Builder+

Filter firearms by frame/component material.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `material` | string | yes | Material name |
| `component` | string | yes | Which component the material applies to |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | One page of results |
| `data[].id` | string | Slug of the firearm. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].material` | string \| null | The material recorded for the component that was filtered on. Matched case-insensitively, returned as stored. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "m16a1",
      "name": "M16A1",
      "manufacturerId": "colt",
      "categoryId": "rifle",
      "material": "Steel"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/by-material?material=polymer&component=frame' \
  --header 'X-API-Key: your_key'
```

## client.firearms.byDesigner()

`GET /v1/firearms/by-designer`

Auth: API key required  
Tier: Builder+

Filter firearms by designer name.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `designer` | string | yes | Designer name. Matched as a substring, so a surname alone works |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | One page of results |
| `data[].id` | string | Slug of the firearm. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].designer` | string \| null | Everyone credited on this firearm, as stored, not only the name filtered on. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "m16a1",
      "name": "M16A1",
      "manufacturerId": "colt",
      "categoryId": "rifle",
      "designer": "John Browning"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/by-designer?designer=John%20Browning' \
  --header 'X-API-Key: your_key'
```

## client.firearms.powerRating()

`GET /v1/firearms/power-rating`

Auth: API key required  
Tier: Builder+

Get firearms ranked by computed power rating.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `category` | string | no | Restrict the ranking to a category slug |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | One page of results |
| `data[].id` | string | Slug of the firearm. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].powerRating` | number | Composite score from 0 to 100, the sum of the weighted components in `breakdown`. |
| `data[].breakdown` | object | The weighted components `powerRating` is the sum of. |
| `data[].breakdown.energy` | number | Muzzle-energy component, out of 30. |
| `data[].breakdown.range` | number | Effective-range component, out of 25. |
| `data[].breakdown.fireRate` | number | Rate-of-fire component, out of 20. |
| `data[].breakdown.capacity` | number | Magazine-capacity component, out of 15. |
| `data[].breakdown.mobility` | number | Mobility component, out of 10. Lighter and shorter scores higher. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "browning-m3m",
      "name": "Browning M3M",
      "manufacturerId": "fn-herstal",
      "categoryId": "machine-gun",
      "powerRating": 86.46,
      "breakdown": {
        "energy": 30,
        "range": 23.13,
        "fireRate": 18.33,
        "capacity": 15,
        "mobility": 0
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/power-rating?category=rifle' \
  --header 'X-API-Key: your_key'
```

## client.firearms.timeline()

`GET /v1/firearms/timeline`

Auth: API key required  
Tier: Builder+

Get firearms arranged in a chronological timeline.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | integer | no | Earliest year of introduction |
| `to` | integer | no | Latest year of introduction |
| `category` | string | no | Restrict to a category slug |
| `page` | integer | no | Page number |
| `per_page` | integer | no | Items per page (max 100) |
| `order` | string | no | Sort order |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | TimelineEntry[] | One page of results |
| `data[].id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].yearDiscontinued` | integer \| null | Year production ended, or null while it is still made. |
| `data[].status` | string \| null | 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. |
| `data[].countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "ak-47",
      "name": "AK-47",
      "manufacturerId": "izhmash",
      "categoryId": "rifle",
      "yearIntroduced": 1949,
      "yearDiscontinued": 1959,
      "status": "discontinued",
      "countryOfOrigin": "RU"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/timeline?from=1900&to=1945&category=rifle' \
  --header 'X-API-Key: your_key'
```

## client.firearms.byConflict()

`GET /v1/firearms/by-conflict`

Auth: API key required  
Tier: Studio+

Filter firearms by military conflict.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `conflict` | string | yes | Conflict name |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | One page of results |
| `data[].id` | string | Slug of the firearm. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].conflicts` | object[] | Every conflict this firearm is recorded in, not only the one filtered on. |
| `data[].conflicts[].name` | string | The conflict. |
| `data[].conflicts[].years` | string | When it ran, as written. |
| `data[].conflicts[].sides` | string[] | The belligerents. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "m16a1",
      "name": "M16A1",
      "manufacturerId": "colt",
      "categoryId": "rifle",
      "yearIntroduced": 1967,
      "conflicts": [
        {
          "name": "Vietnam War",
          "years": "1955-1975",
          "sides": [
            "North Vietnam / Viet Cong",
            "South Vietnam / United States"
          ]
        }
      ]
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/by-conflict?conflict=Vietnam%20War' \
  --header 'X-API-Key: your_key'
```

## client.firearms.get()

`GET /v1/firearms/{id}`

Auth: API key required  
Tier: Builder+

Get a single firearm by its slug or ID.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmDetail | A firearm as stored: every specification column we serve, with no nested relations. `FirearmDetail` is this plus them, and `/v1/firearms/{id}/family-tree` returns this shape for `current`, since the walk reads the row and does not load the relations. |
| `data.has3dModel` | integer | Integer flag, `1` when a 3D model is on file. Stored as the database holds it rather than as a boolean. |
| `data.svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data.model3dUrl` | string \| null | GLB model, or null where none is on file. |
| `data.defaultAmmoId` | string \| null | The ammunition load ballistics figures are quoted against, where one is nominated. |
| `data.sourceMuzzleVelocityMps` | number \| null | Muzzle velocity in metres per second **exactly as the source stated it**, kept beside our own figure so a reader can see what was quoted and what was derived. |
| `data.sourceMuzzleEnergyJ` | number \| null | Muzzle energy in joules exactly as the source stated it. |
| `data.sourceEffectiveRangeM` | number \| null | Effective range in metres exactly as the source stated it. |
| `data.sourceMaxRangeM` | number \| null | Maximum range in metres exactly as the source stated it. |
| `data.ballisticsSource` | string \| null | Where the ballistics figures came from, named in prose. |
| `data.ballisticsSourceUrl` | string \| null | URL of the ballistics source, where it is a page rather than a book. |
| `data.id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. Stable: it is what a mirror keys on. |
| `data.name` | string | Display name, as the maker writes it. |
| `data.manufacturerId` | string | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data.categoryId` | string | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data.parentFirearmId` | string \| null | The firearm this one is a variant of, or null when it is not a variant. Variant relationships are what `firearm.variant.updated` webhooks and `/v1/firearms/{id}/variants` follow. |
| `data.variantType` | string \| null | How this record differs from its parent: `optics_ready`, `compact`, `threaded` and so on. Null when the record has no parent. |
| `data.yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data.yearDiscontinued` | integer \| null | Year production ended, or null while it is still made. |
| `data.status` | FirearmStatus \| null | 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. |
| `data.countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data.weightEmptyG` | number \| null | Unloaded weight in grams, without magazine. |
| `data.weightLoadedG` | number \| null | Loaded weight in grams, with a full magazine. |
| `data.overallLengthMm` | number \| null | Overall length in millimetres, stock extended where it folds or collapses. |
| `data.barrelLengthMm` | number \| null | Barrel length in millimetres, measured from the breech face. |
| `data.heightMm` | number \| null | Height in millimetres, including sights and a fitted magazine. |
| `data.widthMm` | number \| null | Width in millimetres at the widest point. |
| `data.sightRadiusMm` | number \| null | Distance between front and rear iron sights in millimetres. Null on a record with no iron sights. |
| `data.actionType` | string \| null | How the action cycles: `short_recoil`, `gas_operated`, `blowback`, `bolt_action` and so on. An open vocabulary: the values in use are published on the schema as `x-gunspec-vocabulary`, and `GET /v1/firearms/action-types` is the live list. |
| `data.firingMechanism` | string \| null | What releases the striker or hammer: `striker_fired`, `hammer_fired`. |
| `data.triggerType` | string \| null | Trigger arrangement: `single_action`, `double_action`, `safe_action`. |
| `data.triggerPullN` | number \| null | Trigger pull weight in newtons. Divide by 4.448 for pounds-force. |
| `data.magazineCapacity` | integer \| null | Rounds in the standard magazine. Null where the firearm is not magazine-fed. |
| `data.magazineType` | string \| null | Magazine pattern: `detachable_box`, `internal`, `drum`, `belt`. |
| `data.muzzleVelocityMps` | number \| null | Muzzle velocity in metres per second, for the default load. |
| `data.muzzleEnergyJ` | number \| null | Muzzle energy in joules, for the default load. |
| `data.effectiveRangeM` | number \| null | Effective range in metres against a point target. |
| `data.maxRangeM` | number \| null | Maximum range in metres. Where the projectile lands, not where it is useful. |
| `data.rateOfFireRpm` | integer \| null | Cyclic rate in rounds per minute. Null on anything that is not automatic. |
| `data.barrelRifling` | string \| null | Rifling description, e.g. `6 grooves, right-hand twist`. |
| `data.riflingTwistMm` | number \| null | Rifling twist rate in millimetres per turn. Divide by 25.4 for inches. |
| `data.numberOfGrooves` | integer \| null | Number of grooves cut in the bore. |
| `data.frameMaterial` | string \| null | What the frame or receiver is made of. |
| `data.slideMaterial` | string \| null | What the slide is made of. Null on anything without one. |
| `data.barrelMaterial` | string \| null | What the barrel is made of. |
| `data.stockMaterial` | string \| null | What the stock or furniture is made of. |
| `data.finish` | string \| null | Surface finish, e.g. `nitride`, `parkerized`, `cerakote`. |
| `data.safetyMechanisms` | string[] | Safety mechanisms the design carries. |
| `data.features` | string[] | Notable features of the design. |
| `data.feedSystems` | string[] | How the firearm is fed. |
| `data.gameDamage` | integer \| null | Editorial game statistic, 0-100. **Not a measured figure.** These are balance numbers for game use and are not derived from the ballistics above. |
| `data.gameAccuracy` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameRange` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameFireRate` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameMobility` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameRecoilControl` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameReloadSpeed` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameConcealment` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.description` | string \| null | Prose summary of the record, where one has been written. |
| `data.notes` | string \| null | Free-text notes that do not belong in a specific field. |
| `data.designer` | string \| null | Person or team credited with the design, where it is known. |
| `data.foldedLengthMm` | number \| null | Length with stock folded (mm) |
| `data.alternateNames` | string[] | Other names and designations the same firearm is known by. |
| `data.firingModes` | string[] | Firing modes the selector offers. |
| `data.conflicts` | object[] | Conflicts the firearm saw service in. |
| `data.conflicts[].name` | string | The conflict. |
| `data.conflicts[].years` | string | When it ran, as written. |
| `data.conflicts[].sides` | string[] | The belligerents. |
| `data.productionNumbers` | object \| null | How many were made, and over what period. |
| `data.productionNumbers.estimated_total` | integer \| null | Units believed produced. |
| `data.productionNumbers.production_years` | string \| null | The production run, as written. |
| `data.productionNumbers.notes` | string \| null | What the estimate covers. |
| `data.lore` | string \| null | Historical trivia / game-dev flavor text |
| `data.sources` | string[] | Source URLs the record was compiled from. Check a specific figure against these rather than against `dataConfidence`. The same list is in `provenance.sources`. |
| `data.verifiedAt` | string \| null | When a source was last read against the record's figures. Null until one has been. The same value is in `provenance.verifiedAt`. |
| `data.verifiedFields` | string[] | The figures a source confirmed: read off the manufacturer's own page, within the published tolerance, and accepted by a person. The same list is in `provenance.verifiedFields`. |
| `data.dataConfidence` | number \| null | How completely the record is specified and how well it is sourced, 0 to 1. A record-level completeness and provenance measure, **not** a per-field probability of correctness. Use it to rank and triage; use `sources` to verify an individual number. |
| `data.createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data.updatedAt` | string | When a field we serve last changed. Maintained by the database rather than by whatever wrote the row, so it moves for a corrected specification and does not move for a re-run of the importer. |
| `data.version` | string \| null | Opaque fingerprint of this record. It changes when and only when a field we serve changes, so a client holding the same value already has the record and need not fetch it. Equality is the only supported operation: do not parse it, and do not assume how it is computed. Null means the record has not been written since versioning began, not that it is unchanged. |
| `data.provenance` | Provenance | Where a record's figures came from and how far they have been checked, in one place. `sources` are the pages consulted; check a specific figure against those. `sourceKinds` says what each page is and `bestSourceKind` the strongest of them, on the hierarchy `SourceKind` defines: a maker or standards-body page is evidence for a figure, a retailer or forum page is evidence the item exists. `dataConfidence` is the 0 to 1 score set from what was actually sourced and never raised by hand. `verifiedAt` and `verifiedFields` say when a source was last read against the record and which fields it stated; null means the row is still seed knowledge. `updatedAt` and `version` are the same cache signals the record carries at the top level. |
| `data.provenance.sources` | string[] | The pages consulted when the record was compiled, as an array. Check a specific figure against these rather than against `dataConfidence`. |
| `data.provenance.sourceKinds` | object[] | What each cited page is, in `sources` order, on the `SourceKind` hierarchy. A host not in the source map is `other`, never guessed. |
| `data.provenance.bestSourceKind` | SourceKind \| null | The strongest kind among the citations, or null when nothing is cited. `manufacturer` or `standards_body` means a figure can be checked against an authority; `retailer` or `community` alone means the record is still supported by copies of copies. |
| `data.provenance.makerSource` | string \| null | The first cited page on the record's own manufacturer's website, or null when it cites none. A page on another maker's site does not count. Cited from the maker is not the same as checked against it: `verifiedFields` names the figures that were read against a source. |
| `data.provenance.evidence` | string | How far the record's figures are backed, strongest first, for a badge: `verified` when at least one figure was read against a source (`verifiedFields`), `maker` when it cites a page on its own manufacturer's site (`makerSource`), `authority` when it cites a maker, standards body or government page, `secondary` when it cites only press, retailers, references or forums, `none` when it cites nothing. Derived from the fields beside it. |
| `data.provenance.checks` | object | The checks the record has been through, on a single record's response only: every task the data agent (Redback) or a person was given about it, what each proposed, the pages it read, and whether a person accepted it. Nothing an agent proposes reaches the record until a person accepts it, so this is the trail behind `verifiedFields`. Who reviewed is not published; an agent's instance name is. |
| `data.provenance.dataConfidence` | number \| null | The 0 to 1 score set from what was actually sourced, never raised by hand. See the confidence bands in the docs. |
| `data.provenance.verifiedAt` | string \| null | When a source was last read against this record. Null means the row is still seed knowledge. |
| `data.provenance.verifiedFields` | string[] | Which fields the source stated, as an array. Everything else on a verified row is still unverified. Null where no source has been read. |
| `data.provenance.specSource` | string \| null | Cartridges only: the page the drawing figures were taken from. Null on every other kind of record. |
| `data.provenance.updatedAt` | string | When a served column last changed; the same value the record carries at the top level. |
| `data.provenance.version` | string \| null | The record's content version, equal to the top-level `version`; equal versions mean equal data. |
| `data.manufacturer` | Manufacturer \| null | Null when the record names a manufacturer id the catalog no longer holds. |
| `data.category` | Category \| null | Null when the record names a category id the catalog no longer holds. |
| `data.calibers` | FirearmCaliber[] | Every cartridge this firearm is chambered for, primary first. |
| `data.calibers[].caliberId` | string | Slug of the cartridge. Resolve against `/v1/calibers/{id}`. |
| `data.calibers[].isPrimary` | integer | Integer flag, `1` on the chambering the firearm is normally sold in. A firearm may be chambered for several cartridges but has at most one primary. |
| `data.calibers[].name` | string | Display name, as the maker writes it. |
| `data.calibers[].natoDesignation` | string \| null | NATO designation where the cartridge has one, e.g. `9mm NATO`. Null otherwise. |
| `data.calibers[].bulletDiameterMm` | number \| null | Bullet diameter in millimetres. |
| `data.calibers[].caseLengthMm` | number \| null | Case length in millimetres. |
| `data.calibers[].cartridgeType` | string \| null | Head type: `rimmed`, `rimless`, `semi_rimmed`, `rebated_rim`, `belted`, plus historical values like `cap_and_ball`. **Not the broad class.** The registry said `pistol`/`rifle`/`shotgun` until the column was read, and it holds neither. |
| `data.calibers[].neckDiameterMm` | number \| null | Case neck outside diameter in millimetres. |
| `data.calibers[].shoulderDiameterMm` | number \| null | Bottleneck cases only. |
| `data.calibers[].baseDiameterMm` | number \| null | Case base diameter in millimetres, ahead of the extractor groove. |
| `data.calibers[].rimDiameterMm` | number \| null | Rim diameter in millimetres. |
| `data.calibers[].rimThicknessMm` | number \| null | Rim thickness in millimetres. |
| `data.images` | FirearmImage[] | Every image on file, silhouette first. Each carries an absolute, directly fetchable `url`. |
| `data.images[].id` | integer | Autoincrementing image id. Numeric, unlike the catalog slugs. |
| `data.images[].firearmId` | string | Slug of the firearm this row belongs to. |
| `data.images[].url` | string | Absolute, directly fetchable URL. |
| `data.images[].type` | ImageType \| null | Role of the image: `primary`, `gallery`, `thumbnail`, `svg`, `render`. |
| `data.images[].source` | string \| null | Where the record came from: a curated entry, an inference, or an import. |
| `data.images[].license` | string \| null | Licence the asset is held under, where one is recorded. |
| `data.images[].kind` | MediaKind \| null | What the asset is: `silhouette`, `render`, `photo`, `schematic`, `model`. `/v1/firearms/{id}/media/{selector}` addresses an asset by this. |
| `data.images[].storage` | string \| null | Where the bytes live: `cdn` for a static path, `r2` for an upload we serve. |
| `data.images[].author` | string \| null | Who made the asset, where it is credited. |
| `data.images[].sourceUrl` | string \| null | Page the asset or document was taken from. |
| `data.images[].alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data.images[].width` | integer \| null | Pixel width, where it is known. |
| `data.users` | FirearmUser[] | Forces and agencies that have adopted it. |
| `data.users[].id` | integer | Autoincrementing row id. |
| `data.users[].firearmId` | string | Slug of the firearm this row belongs to. |
| `data.users[].userName` | string | The force, agency or organisation that adopted it. |
| `data.users[].userType` | string \| null | What kind of user: `military`, `police`, `civilian`. |
| `data.users[].countryCode` | string \| null | ISO 3166-1 alpha-2 code of the user's country. |
| `data.users[].adoptedYear` | integer \| null | Year of adoption, where it is known. |
| `data.users[].designation` | string \| null | What that user calls it, where it differs from the maker's name. |
| `data.schematics` | FirearmSchematic[] | Exploded-view and parts diagrams on file, empty where none have been sourced. |
| `data.schematics[].id` | integer | Autoincrementing schematic id. |
| `data.schematics[].firearmId` | string | Slug of the firearm this row belongs to. |
| `data.schematics[].title` | string | What the document is called. |
| `data.schematics[].type` | SchematicType | What a schematic document is. |
| `data.schematics[].url` | string | Absolute, directly fetchable URL. |
| `data.schematics[].format` | string \| null | File format, e.g. `pdf`. |
| `data.schematics[].version` | string \| null | Revision of the document, where the publisher versions it. |
| `data.schematics[].manufacturer` | string \| null | Publisher of the document, often but not always the maker. |
| `data.schematics[].source` | string \| null | Where the document came from. |
| `data.schematics[].sourceUrl` | string \| null | Page the asset or document was taken from. |
| `data.schematics[].author` | string \| null | Who made the asset, where it is credited. |
| `data.schematics[].license` | string \| null | Licence the asset is held under, where one is recorded. |

```json
{
  "success": true,
  "data": {
    "has3dModel": 0,
    "svgLineArtUrl": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/media/silhouette",
    "model3dUrl": null,
    "defaultAmmoId": "m882",
    "sourceMuzzleVelocityMps": 375,
    "sourceMuzzleEnergyJ": 565,
    "sourceEffectiveRangeM": 50,
    "sourceMaxRangeM": 1800,
    "ballisticsSource": "Manufacturer specification sheet",
    "ballisticsSourceUrl": "https://eu.glock.com/en/pistols/g17",
    "id": "glock-g17-gen5",
    "name": "Glock 17 Gen5",
    "manufacturerId": "glock",
    "categoryId": "pistol",
    "parentFirearmId": "glock-17",
    "variantType": "generation",
    "yearIntroduced": 2017,
    "yearDiscontinued": null,
    "status": "in_production",
    "countryOfOrigin": "AT",
    "weightEmptyG": 625,
    "weightLoadedG": 905,
    "overallLengthMm": 204,
    "barrelLengthMm": 114,
    "heightMm": 139,
    "widthMm": 34,
    "sightRadiusMm": 165,
    "actionType": "short_recoil",
    "firingMechanism": "striker-fired",
    "triggerType": "safe-action",
    "triggerPullN": 28,
    "magazineCapacity": 17,
    "magazineType": "detachable-box",
    "muzzleVelocityMps": 375,
    "muzzleEnergyJ": 570,
    "effectiveRangeM": 50,
    "maxRangeM": null,
    "rateOfFireRpm": null,
    "barrelRifling": "polygonal",
    "riflingTwistMm": 250,
    "numberOfGrooves": null,
    "frameMaterial": "polymer",
    "slideMaterial": "steel",
    "barrelMaterial": "steel",
    "stockMaterial": null,
    "finish": "nDLC",
    "safetyMechanisms": [
      "manual_safety_selector",
      "half_cock_notch"
    ],
    "features": [
      "collapsible_stock",
      "accessory_rail"
    ],
    "feedSystems": [
      "detachable_box_magazine"
    ],
    "gameDamage": null,
    "gameAccuracy": null,
    "gameRange": null,
    "gameFireRate": null,
    "gameMobility": null,
    "gameRecoilControl": null,
    "gameReloadSpeed": null,
    "gameConcealment": null,
    "description": null,
    "notes": null,
    "designer": "Mikhail Kalashnikov",
    "foldedLengthMm": 490,
    "alternateNames": [
      "Avtomat Kalashnikova",
      "Type 56",
      "Kalash"
    ],
    "firingModes": [
      "semi_automatic",
      "full_automatic"
    ],
    "conflicts": [
      {
        "name": "Vietnam War",
        "years": "1955-1975",
        "sides": [
          "North Vietnam / Viet Cong",
          "South Vietnam / United States"
        ]
      }
    ],
    "productionNumbers": {
      "estimated_total": 75000000,
      "production_years": "1947-1975",
      "notes": "Approximately 75 million AK-47 type rifles produced."
    },
    "lore": "The AK-47 is so iconic it appears on the national flag of Mozambique.",
    "sources": [
      "string"
    ],
    "verifiedAt": "2026-09-25",
    "verifiedFields": [
      "barrel_length_mm",
      "weight_empty_g"
    ],
    "dataConfidence": 0.95,
    "createdAt": "2025-01-15 12:00:00",
    "updatedAt": "2026-09-10 06:42:19",
    "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2",
    "provenance": {
      "sources": [
        "https://www.glock.com/en/products/pistols/g17-gen5"
      ],
      "sourceKinds": [
        {
          "url": "https://www.glock.com/en/products/pistols/g17-gen5",
          "kind": "manufacturer",
          "archivedFrom": null,
          "rank": 0,
          "tier": "authority",
          "basis": "declared",
          "evidence": null
        }
      ],
      "bestSourceKind": "manufacturer",
      "makerSource": "https://www.glock.com/en/products/pistols/g17-gen5",
      "evidence": "maker",
      "checks": {
        "raised": 3,
        "open": 0,
        "history": [
          {
            "task": "verify:firearms:glock-g17-gen5",
            "number": 1204,
            "check": "quality:firearm-weight-fits-its-size",
            "kind": "verify",
            "agent": "redback",
            "worker": "redback-046535",
            "status": "resolved",
            "decidedAt": "2026-09-25 07:42:39",
            "proposed": "Read https://us.glock.com/en/pistols/g17-gen5: agrees on barrel_length, weight.",
            "fields": [
              "barrel_length_mm",
              "weight_empty_g"
            ],
            "sources": [
              "https://us.glock.com/en/pistols/g17-gen5"
            ],
            "review": {
              "decision": "accepted",
              "at": null
            },
            "finding": {
              "why": "2.22 g/cm3 against a submachine-gun / carbon-steel median of 0.64; check the weight and the dimensions",
              "evidence": {
                "apparentDensity": 2.218,
                "categoryMedian": 0.635
              }
            }
          }
        ]
      },
      "dataConfidence": 0.95,
      "verifiedAt": "2026-09-08",
      "verifiedFields": [
        "barrel_length_mm",
        "weight_empty_g"
      ],
      "specSource": null,
      "updatedAt": "2026-09-08 04:10:22",
      "version": "ba5c1d9e06c279a5"
    },
    "manufacturer": {
      "id": "glock",
      "name": "Glock Ges.m.b.H.",
      "countryCode": "AT",
      "foundedYear": 1963,
      "status": "active",
      "defunctYear": null,
      "stateOwned": false,
      "makesFirearms": true,
      "parentId": null,
      "predecessorId": null,
      "website": "https://www.glock.com",
      "logoUrl": null,
      "description": null,
      "createdAt": "2025-01-15 12:00:00",
      "updatedAt": "2026-09-10 06:42:19",
      "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2"
    },
    "category": {
      "id": "pistol",
      "name": "Pistol",
      "description": "Handguns designed to be fired with one or two hands"
    },
    "calibers": [
      {
        "caliberId": "9x19mm-parabellum",
        "isPrimary": 1,
        "name": "9x19mm Parabellum",
        "natoDesignation": "9mm NATO",
        "bulletDiameterMm": 9.01,
        "caseLengthMm": 19.15,
        "cartridgeType": "rimless",
        "neckDiameterMm": 9.65,
        "shoulderDiameterMm": null,
        "baseDiameterMm": 9.93,
        "rimDiameterMm": 9.96,
        "rimThicknessMm": 1.27,
        "overallLengthMm": 29.69,
        "bulletLengthMm": 15.5,
        "typicalBulletWeightG": 7.45,
        "primerType": "small_pistol",
        "caseShape": "tapered",
        "caseMaterial": "brass",
        "projectileKind": "bullet",
        "bulletProfile": "round_nose",
        "closure": "bullet",
        "markingColor": null
      }
    ],
    "images": [
      {
        "id": 1,
        "firearmId": "glock-g17-gen5",
        "url": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/images/3076",
        "type": "svg",
        "source": "manufacturer",
        "license": "fair-use",
        "kind": "silhouette",
        "storage": "cdn",
        "author": null,
        "sourceUrl": null,
        "alt": null,
        "width": 1200,
        "height": 800,
        "sortOrder": 0
      }
    ],
    "users": [
      {
        "id": 1,
        "firearmId": "glock-g17-gen5",
        "userName": "Austrian Armed Forces",
        "userType": "military",
        "countryCode": "AT",
        "adoptedYear": 1982,
        "designation": "Pistole 80"
      }
    ],
    "schematics": [
      {
        "id": 12,
        "firearmId": "glock-g17-gen5",
        "title": "Exploded view",
        "type": "blueprint",
        "url": "https://assets.gunspec.io/firearms/schematics/glock-g17-gen5-exploded.pdf",
        "format": "pdf",
        "version": null,
        "manufacturer": "Glock Ges.m.b.H.",
        "source": null,
        "sourceUrl": null,
        "author": null,
        "license": null,
        "createdAt": "2026-05-26 12:36:27"
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getVariants()

`GET /v1/firearms/{id}/variants`

Auth: API key required  
Tier: Explorer+

Get all variants of a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Firearm slug |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmVariant[] | The response payload. |
| `data[].id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].variantType` | string \| null | How this record differs from its parent: `optics_ready`, `compact`, `threaded` and so on. Null when the record has no parent. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].status` | FirearmStatus \| null | 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. |

```json
{
  "success": true,
  "data": [
    {
      "id": "glock-g17-gen5-mos",
      "name": "Glock 17 Gen5 MOS",
      "variantType": "optics-ready",
      "yearIntroduced": 2018,
      "status": "in_production"
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/ak-47/variants' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getImages()

`GET /v1/firearms/{id}/images`

Auth: API key required  
Tier: Explorer+

Get images for a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmImage[] | The response payload. |
| `data[].id` | integer | Autoincrementing image id. Numeric, unlike the catalog slugs. |
| `data[].firearmId` | string | Slug of the firearm this row belongs to. |
| `data[].url` | string | Absolute, directly fetchable URL. |
| `data[].type` | ImageType \| null | Role of the image: `primary`, `gallery`, `thumbnail`, `svg`, `render`. |
| `data[].source` | string \| null | Where the record came from: a curated entry, an inference, or an import. |
| `data[].license` | string \| null | Licence the asset is held under, where one is recorded. |
| `data[].kind` | MediaKind \| null | What the asset is: `silhouette`, `render`, `photo`, `schematic`, `model`. `/v1/firearms/{id}/media/{selector}` addresses an asset by this. |
| `data[].storage` | string \| null | Where the bytes live: `cdn` for a static path, `r2` for an upload we serve. |
| `data[].author` | string \| null | Who made the asset, where it is credited. |
| `data[].sourceUrl` | string \| null | Page the asset or document was taken from. |
| `data[].alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data[].width` | integer \| null | Pixel width, where it is known. |
| `data[].height` | integer \| null | Pixel height, where it is known. |
| `data[].sortOrder` | integer | Position within its list, ascending. |

```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "firearmId": "glock-g17-gen5",
      "url": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/images/3076",
      "type": "svg",
      "source": "manufacturer",
      "license": "fair-use",
      "kind": "silhouette",
      "storage": "cdn",
      "author": null,
      "sourceUrl": null,
      "alt": null,
      "width": 1200,
      "height": 800,
      "sortOrder": 0
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/images' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getGameStats()

`GET /v1/firearms/{id}/game-stats`

Auth: API key required  
Tier: Builder+

Get computed game statistics for a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmGameStats | The response payload |
| `data.id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. |
| `data.name` | string | Display name, as the maker writes it. |
| `data.gameDamage` | integer \| null | Editorial game statistic, 0-100. **Not a measured figure.** These are balance numbers for game use and are not derived from the ballistics above. |
| `data.gameAccuracy` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameRange` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameFireRate` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameMobility` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameRecoilControl` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameReloadSpeed` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameConcealment` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |

```json
{
  "success": true,
  "data": {
    "id": "glock-g17-gen5",
    "name": "Glock 17 Gen5",
    "gameDamage": 45,
    "gameAccuracy": 72,
    "gameRange": 35,
    "gameFireRate": 68,
    "gameMobility": 85,
    "gameRecoilControl": 70,
    "gameReloadSpeed": 75,
    "gameConcealment": 80
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/game-stats' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getDimensions()

`GET /v1/firearms/{id}/dimensions`

Auth: API key required  
Tier: Builder+

Get physical dimensions for a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmDimensions | Physical dimensions in both metric and imperial units. |
| `data.id` | string | Slug of the firearm. |
| `data.name` | string | Display name, as the maker writes it. |
| `data.metric` | object | Every dimension in millimetres and grams. |
| `data.metric.weightEmptyG` | number \| null | Unloaded weight in grams, without magazine. |
| `data.metric.weightLoadedG` | number \| null | Loaded weight in grams, with a full magazine. |
| `data.metric.overallLengthMm` | number \| null | Overall length in millimetres, stock extended where it folds or collapses. |
| `data.metric.barrelLengthMm` | number \| null | Barrel length in millimetres, measured from the breech face. |
| `data.metric.heightMm` | number \| null | Height in millimetres, including sights and a fitted magazine. |
| `data.metric.widthMm` | number \| null | Width in millimetres at the widest point. |
| `data.metric.foldedLengthMm` | number \| null | Overall length in millimetres with the stock folded or collapsed. Null where it does not. |
| `data.imperial` | object | The same measurements converted to pounds and inches. |
| `data.imperial.weightEmptyLbs` | number \| null | Unloaded weight in pounds, without magazine. Converted from `weightEmptyG`. |
| `data.imperial.weightLoadedLbs` | number \| null | Loaded weight in pounds, with a full magazine. Converted from `weightLoadedG`. |
| `data.imperial.overallLengthIn` | number \| null | Overall length in inches, stock extended where it folds or collapses. Converted from `overallLengthMm`. |
| `data.imperial.barrelLengthIn` | number \| null | Barrel length in inches, measured from the breech face. Converted from `barrelLengthMm`. |
| `data.imperial.heightIn` | number \| null | Height in inches, including sights and a fitted magazine. Converted from `heightMm`. |
| `data.imperial.widthIn` | number \| null | Width in inches at the widest point. Converted from `widthMm`. |
| `data.imperial.foldedLengthIn` | number \| null | Overall length in inches with the stock folded or collapsed. Null where it does not. Converted from `foldedLengthMm`. |

```json
{
  "success": true,
  "data": {
    "id": "glock-g17-gen5",
    "name": "Glock 17 Gen5",
    "metric": {
      "weightEmptyG": 625,
      "weightLoadedG": 915,
      "overallLengthMm": 202,
      "barrelLengthMm": 114,
      "heightMm": 139,
      "widthMm": 34,
      "foldedLengthMm": null
    },
    "imperial": {
      "weightEmptyLbs": 1.378,
      "weightLoadedLbs": 2.017,
      "overallLengthIn": 7.953,
      "barrelLengthIn": 4.488,
      "heightIn": 5.472,
      "widthIn": 1.339,
      "foldedLengthIn": null
    }
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/dimensions' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getUsers()

`GET /v1/firearms/{id}/users`

Auth: API key required  
Tier: Explorer+

Get known military/law-enforcement adopters of a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmUser[] | The response payload. |
| `data[].id` | integer | Autoincrementing row id. |
| `data[].firearmId` | string | Slug of the firearm this row belongs to. |
| `data[].userName` | string | The force, agency or organisation that adopted it. |
| `data[].userType` | string \| null | What kind of user: `military`, `police`, `civilian`. |
| `data[].countryCode` | string \| null | ISO 3166-1 alpha-2 code of the user's country. |
| `data[].adoptedYear` | integer \| null | Year of adoption, where it is known. |
| `data[].designation` | string \| null | What that user calls it, where it differs from the maker's name. |

```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "firearmId": "glock-g17-gen5",
      "userName": "Austrian Armed Forces",
      "userType": "military",
      "countryCode": "AT",
      "adoptedYear": 1982,
      "designation": "Pistole 80"
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/users' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getFamilyTree()

`GET /v1/firearms/{id}/family-tree`

Auth: API key required  
Tier: Builder+

Get the family tree (lineage) of a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Firearm slug |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FamilyTree | The firearm plus its ancestor and descendant variants, walked recursively. |
| `data.ancestors` | FamilyMember[] | The parent chain, nearest first, up to 20 deep. |
| `data.ancestors[].id` | string | URL-safe slug identifying the related firearm, e.g. `type-58`. |
| `data.ancestors[].name` | string | Display name, as the maker writes it. |
| `data.ancestors[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data.ancestors[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data.ancestors[].parentFirearmId` | string \| null | The firearm this one is a variant of, or null when it is not a variant. Variant relationships are what `firearm.variant.updated` webhooks and `/v1/firearms/{id}/variants` follow. |
| `data.ancestors[].variantType` | string \| null | How this record differs from its parent: `optics_ready`, `compact`, `threaded` and so on. Null when the record has no parent. |
| `data.ancestors[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data.ancestors[].status` | FirearmStatus \| null | 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. |
| `data.ancestors[].countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data.ancestors[].svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data.ancestors[].updatedAt` | string | When a field we serve last changed. Maintained by the database rather than by whatever wrote the row, so it moves for a corrected specification and does not move for a re-run of the importer. |
| `data.ancestors[].version` | string \| null | Opaque fingerprint of this record. It changes when and only when a field we serve changes, so a client holding the same value already has the record and need not fetch it. Equality is the only supported operation: do not parse it, and do not assume how it is computed. Null means the record has not been written since versioning began, not that it is unchanged. |
| `data.current` | FirearmRecord | A firearm as stored: every specification column we serve, with no nested relations. `FirearmDetail` is this plus them, and `/v1/firearms/{id}/family-tree` returns this shape for `current`, since the walk reads the row and does not load the relations. |
| `data.current.has3dModel` | integer | Integer flag, `1` when a 3D model is on file. Stored as the database holds it rather than as a boolean. |
| `data.current.svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data.current.model3dUrl` | string \| null | GLB model, or null where none is on file. |
| `data.current.defaultAmmoId` | string \| null | The ammunition load ballistics figures are quoted against, where one is nominated. |
| `data.current.sourceMuzzleVelocityMps` | number \| null | Muzzle velocity in metres per second **exactly as the source stated it**, kept beside our own figure so a reader can see what was quoted and what was derived. |
| `data.current.sourceMuzzleEnergyJ` | number \| null | Muzzle energy in joules exactly as the source stated it. |
| `data.current.sourceEffectiveRangeM` | number \| null | Effective range in metres exactly as the source stated it. |
| `data.current.sourceMaxRangeM` | number \| null | Maximum range in metres exactly as the source stated it. |
| `data.current.ballisticsSource` | string \| null | Where the ballistics figures came from, named in prose. |
| `data.current.ballisticsSourceUrl` | string \| null | URL of the ballistics source, where it is a page rather than a book. |
| `data.current.id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. Stable: it is what a mirror keys on. |
| `data.current.name` | string | Display name, as the maker writes it. |
| `data.descendants` | FamilyMember[] | Everything derived from it, breadth-first, up to 50 rows. |
| `data.descendants[].id` | string | URL-safe slug identifying the related firearm, e.g. `type-58`. |
| `data.descendants[].name` | string | Display name, as the maker writes it. |
| `data.descendants[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data.descendants[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data.descendants[].parentFirearmId` | string \| null | The firearm this one is a variant of, or null when it is not a variant. Variant relationships are what `firearm.variant.updated` webhooks and `/v1/firearms/{id}/variants` follow. |
| `data.descendants[].variantType` | string \| null | How this record differs from its parent: `optics_ready`, `compact`, `threaded` and so on. Null when the record has no parent. |
| `data.descendants[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data.descendants[].status` | FirearmStatus \| null | 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. |
| `data.descendants[].countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data.descendants[].svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data.descendants[].updatedAt` | string | When a field we serve last changed. Maintained by the database rather than by whatever wrote the row, so it moves for a corrected specification and does not move for a re-run of the importer. |
| `data.descendants[].version` | string \| null | Opaque fingerprint of this record. It changes when and only when a field we serve changes, so a client holding the same value already has the record and need not fetch it. Equality is the only supported operation: do not parse it, and do not assume how it is computed. Null means the record has not been written since versioning began, not that it is unchanged. |

```json
{
  "success": true,
  "data": {
    "ancestors": [
      {
        "id": "type-58",
        "name": "Type 58",
        "manufacturerId": "north-korean-state-arsenals",
        "categoryId": "assault-rifle",
        "parentFirearmId": "ak-47",
        "variantType": "foreign_production",
        "yearIntroduced": 1958,
        "status": "in_production",
        "countryOfOrigin": "KP",
        "svgLineArtUrl": "https://api.gunspec.io/v1/firearms/type-58/media/silhouette",
        "updatedAt": "2026-09-10 06:42:19",
        "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2"
      }
    ],
    "current": {
      "has3dModel": 0,
      "svgLineArtUrl": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/media/silhouette",
      "model3dUrl": null,
      "defaultAmmoId": "m882",
      "sourceMuzzleVelocityMps": 375,
      "sourceMuzzleEnergyJ": 565,
      "sourceEffectiveRangeM": 50,
      "sourceMaxRangeM": 1800,
      "ballisticsSource": "Manufacturer specification sheet",
      "ballisticsSourceUrl": "https://eu.glock.com/en/pistols/g17",
      "id": "glock-g17-gen5",
      "name": "Glock 17 Gen5",
      "manufacturerId": "glock",
      "categoryId": "pistol",
      "parentFirearmId": "glock-17",
      "variantType": "generation",
      "yearIntroduced": 2017,
      "yearDiscontinued": null,
      "status": "in_production",
      "countryOfOrigin": "AT",
      "weightEmptyG": 625,
      "weightLoadedG": 905,
      "overallLengthMm": 204,
      "barrelLengthMm": 114,
      "heightMm": 139,
      "widthMm": 34,
      "sightRadiusMm": 165,
      "actionType": "short_recoil",
      "firingMechanism": "striker-fired",
      "triggerType": "safe-action",
      "triggerPullN": 28,
      "magazineCapacity": 17,
      "magazineType": "detachable-box",
      "muzzleVelocityMps": 375,
      "muzzleEnergyJ": 570,
      "effectiveRangeM": 50,
      "maxRangeM": null,
      "rateOfFireRpm": null,
      "barrelRifling": "polygonal",
      "riflingTwistMm": 250,
      "numberOfGrooves": null,
      "frameMaterial": "polymer",
      "slideMaterial": "steel",
      "barrelMaterial": "steel",
      "stockMaterial": null,
      "finish": "nDLC",
      "safetyMechanisms": [
        "manual_safety_selector",
        "half_cock_notch"
      ],
      "features": [
        "collapsible_stock",
        "accessory_rail"
      ],
      "feedSystems": [
        "detachable_box_magazine"
      ],
      "gameDamage": null,
      "gameAccuracy": null,
      "gameRange": null,
      "gameFireRate": null,
      "gameMobility": null,
      "gameRecoilControl": null,
      "gameReloadSpeed": null,
      "gameConcealment": null,
      "description": null,
      "notes": null,
      "designer": "Mikhail Kalashnikov",
      "foldedLengthMm": 490,
      "alternateNames": [
        "Avtomat Kalashnikova",
        "Type 56",
        "Kalash"
      ],
      "firingModes": [
        "semi_automatic",
        "full_automatic"
      ],
      "conflicts": [
        {
          "name": "Vietnam War",
          "years": "1955-1975",
          "sides": [
            "North Vietnam / Viet Cong",
            "South Vietnam / United States"
          ]
        }
      ],
      "productionNumbers": {
        "estimated_total": 75000000,
        "production_years": "1947-1975",
        "notes": "Approximately 75 million AK-47 type rifles produced."
      },
      "lore": "The AK-47 is so iconic it appears on the national flag of Mozambique.",
      "sources": [
        "string"
      ],
      "verifiedAt": "2026-09-25",
      "verifiedFields": [
        "barrel_length_mm",
        "weight_empty_g"
      ],
      "dataConfidence": 0.95,
      "createdAt": "2025-01-15 12:00:00",
      "updatedAt": "2026-09-10 06:42:19",
      "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2",
      "provenance": {
        "sources": [
          "https://www.glock.com/en/products/pistols/g17-gen5"
        ],
        "sourceKinds": [
          {
            "url": "https://www.glock.com/en/products/pistols/g17-gen5",
            "kind": "manufacturer",
            "archivedFrom": null,
            "rank": 0,
            "tier": "authority",
            "basis": "declared",
            "evidence": null
          }
        ],
        "bestSourceKind": "manufacturer",
        "makerSource": "https://www.glock.com/en/products/pistols/g17-gen5",
        "evidence": "maker",
        "checks": {
          "raised": 3,
          "open": 0,
          "history": [
            {
              "task": "verify:firearms:glock-g17-gen5",
              "number": 1204,
              "check": "quality:firearm-weight-fits-its-size",
              "kind": "verify",
              "agent": "redback",
              "worker": "redback-046535",
              "status": "resolved",
              "decidedAt": "2026-09-25 07:42:39",
              "proposed": "Read https://us.glock.com/en/pistols/g17-gen5: agrees on barrel_length, weight.",
              "fields": [
                "barrel_length_mm",
                "weight_empty_g"
              ],
              "sources": [
                "https://us.glock.com/en/pistols/g17-gen5"
              ],
              "review": {
                "decision": "accepted",
                "at": null
              },
              "finding": {
                "why": "2.22 g/cm3 against a submachine-gun / carbon-steel median of 0.64; check the weight and the dimensions",
                "evidence": {
                  "apparentDensity": 2.218,
                  "categoryMedian": 0.635
                }
              }
            }
          ]
        },
        "dataConfidence": 0.95,
        "verifiedAt": "2026-09-08",
        "verifiedFields": [
          "barrel_length_mm",
          "weight_empty_g"
        ],
        "specSource": null,
        "updatedAt": "2026-09-08 04:10:22",
        "version": "ba5c1d9e06c279a5"
      }
    },
    "descendants": [
      {
        "id": "type-58",
        "name": "Type 58",
        "manufacturerId": "north-korean-state-arsenals",
        "categoryId": "assault-rifle",
        "parentFirearmId": "ak-47",
        "variantType": "foreign_production",
        "yearIntroduced": 1958,
        "status": "in_production",
        "countryOfOrigin": "KP",
        "svgLineArtUrl": "https://api.gunspec.io/v1/firearms/type-58/media/silhouette",
        "updatedAt": "2026-09-10 06:42:19",
        "version": "a3f1c2d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2"
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/akm/family-tree' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getSimilar()

`GET /v1/firearms/{id}/similar`

Auth: API key required  
Tier: Explorer+

Get firearms similar to a given firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | SimilarFirearm[] | The response payload. |
| `data[].id` | string | Slug of the similar firearm. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].score` | number | How alike the two records are, 0 to 1. Computed from the specifications, not from what people browse together. |
| `data[].manufacturerId` | string \| null | Slug of the manufacturer that makes it. Resolve against `/v1/manufacturers/{id}`. |
| `data[].categoryId` | string \| null | Slug of the category it belongs to. Resolve against `/v1/categories`. |
| `data[].countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].status` | FirearmStatus \| null | 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. |
| `data[].svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data[].actionType` | string \| null | How the action cycles: `short_recoil`, `gas_operated`, `blowback`, `bolt_action` and so on. An open vocabulary: the values in use are published on the schema as `x-gunspec-vocabulary`, and `GET /v1/firearms/action-types` is the live list. |
| `data[].images` | InlineMediaItem[] | Every image this firearm has, silhouette first. Empty when none are on file. |
| `data[].images[].id` | integer | Autoincrementing image id. |
| `data[].images[].url` | string | Absolute, directly fetchable URL. |
| `data[].images[].kind` | MediaKind | What kind of asset a media row is. |
| `data[].images[].alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data[].images[].width` | integer \| null | Pixel width, where it is known. |
| `data[].images[].height` | integer \| null | Pixel height, where it is known. |

```json
{
  "success": true,
  "data": [
    {
      "id": "sig-sauer-p320-full-size",
      "name": "SIG Sauer P320",
      "score": 0.82,
      "manufacturerId": "sig-sauer",
      "categoryId": "pistol",
      "countryOfOrigin": "US",
      "yearIntroduced": 2014,
      "status": "in_production",
      "svgLineArtUrl": null,
      "actionType": "short_recoil",
      "images": [
        {
          "id": 2572,
          "url": "https://api.gunspec.io/v1/firearms/ak-47/images/2572",
          "kind": "silhouette",
          "alt": "AK-47 right profile",
          "width": 1536,
          "height": 1024
        }
      ]
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/similar' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getAdoptionMap()

`GET /v1/firearms/{id}/adoption-map`

Auth: API key required  
Tier: Studio+

Get the worldwide adoption map for a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | AdoptionMap | Military and law-enforcement adoption of a firearm, grouped by country. |
| `data.firearmId` | string | Slug of the firearm this row belongs to. |
| `data.firearmName` | string | Display name of the firearm the map is for. |
| `data.countries` | object[] | Every country recorded as having adopted it. |
| `data.countries[].code` | string \| null | ISO 3166-1 alpha-2 code of the country. Null where the operator country is unrecorded. |
| `data.countries[].users` | object[] | The organisations in that country recorded as operating the firearm. |

```json
{
  "success": true,
  "data": {
    "firearmId": "ak-47",
    "firearmName": "AK-47",
    "countries": [
      {
        "code": "RU",
        "users": [
          {
            "name": "Russian Ground Forces",
            "type": "military",
            "year": 1949,
            "designation": "AK"
          }
        ]
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/adoption-map' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getGameProfile()

`GET /v1/firearms/{id}/game-profile`

Auth: API key required  
Tier: Builder+

Get the full game profile for a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | GameProfile | Game stats for a firearm plus its derived archetype classification. |
| `data.id` | string | Slug of the firearm this profile is for. |
| `data.name` | string | Display name, as the maker writes it. |
| `data.gameDamage` | integer \| null | Editorial game statistic, 0-100. **Not a measured figure.** These are balance numbers for game use and are not derived from the ballistics above. |
| `data.gameAccuracy` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameRange` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameFireRate` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameMobility` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameRecoilControl` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameReloadSpeed` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.gameConcealment` | integer \| null | Editorial game statistic, 0-100. Not a measured figure. |
| `data.archetype` | GameArchetype | The role a firearm's game profile assigns it. |
| `data.strengths` | string[] | The statistics this firearm scores highest on. |
| `data.weaknesses` | string[] | The statistics it scores lowest on. |

```json
{
  "success": true,
  "data": {
    "id": "glock-g17-gen5",
    "name": "Glock 17 Gen5",
    "gameDamage": 45,
    "gameAccuracy": 70,
    "gameRange": 35,
    "gameFireRate": 55,
    "gameMobility": 85,
    "gameRecoilControl": 65,
    "gameReloadSpeed": 75,
    "gameConcealment": 80,
    "archetype": "sniper",
    "strengths": [
      "mobility",
      "concealment"
    ],
    "weaknesses": [
      "range"
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/game-profile' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getSilhouette()

`GET /v1/firearms/{id}/silhouette`

Auth: API key required  
Tier: Builder+

Get an SVG silhouette (line art) of a firearm.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `format` | string | no | An SVG file (`raw`; `svg` is accepted as the same thing, the name SDK 0.1.x used), an inline data URI, or the SVG source as JSON |
| `stroke_width` | integer | no | Inject a stroke so the line art reads on any background. 0 leaves it unstroked; anything above 20 is clamped to 20 |
| `stroke_color` | string | no | Colour of that stroke. Any CSS colour the browser understands: a name, a hex value, `rgb()`, `oklch()`, `color-mix()`. Quotes, angle brackets, ampersands and backslashes are refused, because the value is written into an SVG attribute |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.id` | string | Slug of the firearm. |
| `data.name` | string | Display name, as the maker writes it. |
| `data.slug` | string | Slug of the line-art file, which is the firearm slug. |
| `data.format` | string | Which JSON form this is: `datauri` or `svg`. |
| `data.mimeType` | string | Media type of the bytes, e.g. `image/png`. |
| `data.dataUri` | string | Present when `format=datauri`: the SVG inlined as an RFC 2397 data URI. |
| `data.svg` | string | Present when `format=json`: the SVG source, for parsing or manipulating in code. |

```json
{
  "success": true,
  "data": {
    "id": "ak-47",
    "name": "AK-47",
    "slug": "ak-47",
    "format": "datauri",
    "mimeType": "image/svg+xml",
    "dataUri": "data:image/svg+xml;base64,PHN2ZyB4bWxu...",
    "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" ...>"
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/silhouette' \
  --header 'X-API-Key: your_key'
```

## client.firearms.calculate()

`GET /v1/firearms/{id}/calculate`

Auth: API key required  
Tier: Builder+

Calculate ballistics for a firearm with specific ammunition.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ammo_id` | string | yes | Ammunition load to fire |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | Muzzle solution adjusted for this firearm barrel length, against its published figures. |
| `data.firearm` | BallisticsFirearm | The firearm the solution was computed for. `barrelLengthMm` is absent when the firearm has no recorded barrel. |
| `data.firearm.id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. |
| `data.firearm.name` | string | Display name, as the maker writes it. |
| `data.firearm.barrelLengthMm` | number | Barrel length in millimetres, measured from the breech face. |
| `data.ammunition` | object | The load the calculation used. |
| `data.ammunition.id` | string | Slug of the ammunition load that was fired. |
| `data.ammunition.name` | string | Display name, as the maker writes it. |
| `data.calculated` | object | The figures derived for the barrel length asked about, rather than the reference one. |
| `data.calculated.muzzleVelocityMps` | number | Muzzle velocity in metres per second, for the default load. |
| `data.calculated.muzzleEnergyJ` | number | Muzzle energy in joules, for the default load. |
| `data.calculated.terminalBallistics` | TerminalBallistics | Terminal performance indices computed at the muzzle. |
| `data.sourceReported` | object | The figures published for this firearm, where the catalog records them. |
| `data.sourceReported.muzzleVelocityMps` | number \| null | Muzzle velocity in metres per second, for the default load. |
| `data.sourceReported.muzzleEnergyJ` | number \| null | Muzzle energy in joules, for the default load. |
| `data.delta` | object | Computed minus source-reported. Null where no source figure exists to compare against. |
| `data.delta.velocityMps` | number \| null | Remaining velocity at this distance, in metres per second. |
| `data.delta.energyJ` | number \| null | Remaining energy at this distance, in joules. |
| `data.error` | string \| null | Why the calculation could not be completed, or null when it succeeded. |

```json
{
  "success": true,
  "data": {
    "firearm": {
      "id": "ak-47",
      "name": "AK-47",
      "barrelLengthMm": 415
    },
    "ammunition": {
      "id": "9mm-124gr-fmj",
      "name": "9mm 124gr FMJ"
    },
    "calculated": {
      "muzzleVelocityMps": 360,
      "muzzleEnergyJ": 510,
      "terminalBallistics": {
        "tkoFactor": 12.9,
        "momentumKgMs": 5.7,
        "sectionalDensity": 0.181,
        "hatcherRSP": 103,
        "energyDensityJCm2": 4135.2
      }
    },
    "sourceReported": {
      "muzzleVelocityMps": 375,
      "muzzleEnergyJ": 540
    },
    "delta": {
      "velocityMps": -15,
      "energyJ": -30
    },
    "error": null
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/calculate?ammo_id=m882' \
  --header 'X-API-Key: your_key'
```

## client.firearms.load()

`GET /v1/firearms/{id}/load`

Auth: API key required  
Tier: Builder+

Load a firearm with ammunition and get combined performance data.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `ammo_id` | string | no | Ammunition load to chamber. Defaults to the firearm default load, then the first common load for its primary caliber. |
| `zero_m` | integer | no | Zero distance in metres, where the bullet crosses the line of sight. Defaults to 25 for a handgun and 100 for a long gun. |
| `sight_height_mm` | number | no | Line of sight above the bore, in mm. Defaults to 20 for a handgun and 38 for a long gun. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.firearm` | BallisticsFirearm | The firearm the solution was computed for. `barrelLengthMm` is absent when the firearm has no recorded barrel. |
| `data.firearm.id` | string | URL-safe slug identifying the firearm, e.g. `glock-g17-gen5`. |
| `data.firearm.name` | string | Display name, as the maker writes it. |
| `data.firearm.barrelLengthMm` | number | Barrel length in millimetres, measured from the breech face. |
| `data.ammunition` | object | The load the calculation used. |
| `data.ammunition.id` | string | Slug of the ammunition load the firearm was chambered with. |
| `data.ammunition.name` | string | Display name, as the maker writes it. |
| `data.ammunition.caliberId` | string | Slug of the cartridge. Resolve against `/v1/calibers/{id}`. |
| `data.ammunition.bulletWeightG` | number | Bullet weight in grams. Divide by 0.0648 for grains. |
| `data.ammunition.bulletType` | string | Bullet construction: `fmj`, `jhp`, `sp`, `match`. |
| `data.calculated` | object | The figures derived for the barrel length asked about, rather than the reference one. |
| `data.calculated.muzzleVelocityMps` | number | Muzzle velocity in metres per second, for the default load. |
| `data.calculated.muzzleEnergyJ` | number | Muzzle energy in joules, for the default load. |
| `data.calculated.effectiveRangeM` | number | Effective range in metres against a point target. |
| `data.calculated.terminalBallistics` | TerminalBallistics | Terminal performance indices computed at the muzzle. |
| `data.sourceReported` | object | The figures published for this firearm, where the catalog records them. |
| `data.sourceReported.muzzleVelocityMps` | number \| null | Muzzle velocity in metres per second, for the default load. |
| `data.sourceReported.muzzleEnergyJ` | number \| null | Muzzle energy in joules, for the default load. |
| `data.sourceReported.effectiveRangeM` | number \| null | Effective range in metres against a point target. |
| `data.zeroing` | Zeroing | How the gun is assumed sighted for `pathCm`. Neither figure is recorded per gun, so each is an assumption the caller may override with `zero_m` and `sight_height_mm`: a handgun zeroed at 25 m with its sights 20 mm above the bore, a long gun at 100 m with an optic 38 mm above it. |
| `data.zeroing.zeroM` | number | The distance at which the bullet crosses the line of sight, in m. |
| `data.zeroing.sightHeightMm` | number | The line of sight above the bore, in mm. |
| `data.trajectory` | TrajectoryPoint[] | The flight path, one row per distance step. |
| `data.trajectory[].distanceM` | number | Distance from the muzzle in metres. |
| `data.trajectory[].velocityMps` | number | Remaining velocity at this distance, in metres per second. |
| `data.trajectory[].energyJ` | number | Remaining energy at this distance, in joules. |
| `data.trajectory[].dropCm` | number | Drop below the bore line at this distance, in cm: the fall under gravity alone, with no sighting. |
| `data.trajectory[].pathCm` | number | Height relative to the line of sight at this distance, in cm, negative below: how a drop table reads, for the answer's `zeroing` (the gun zeroed at `zeroM` with its sights `sightHeightMm` above the bore). Starts at minus the sight height at the muzzle and is 0 at the zero. |
| `data.trajectory[].timeOfFlightS` | number | Time of flight to this distance, in seconds. |
| `data.trajectory[].machNumber` | number | Velocity as a multiple of the speed of sound. Below ~1.2 the projectile is transonic and destabilises. |
| `data.trajectory[].momentumKgMs` | number | Momentum in kilogram-metres per second. |
| `data.trajectory[].tkoFactor` | number | Taylor Knock-Out factor at this distance. |
| `data.trajectory[].energyDensityJCm2` | number | Energy per unit frontal area, in joules per square centimetre. |
| `data.error` | string \| null | Why the calculation could not be completed, or null when it succeeded. |

```json
{
  "success": true,
  "data": {
    "firearm": {
      "id": "ak-47",
      "name": "AK-47",
      "barrelLengthMm": 415
    },
    "ammunition": {
      "id": "m43-ball",
      "name": "M43 Ball",
      "caliberId": "7-62x39mm",
      "bulletWeightG": 7.97,
      "bulletType": "FMJ"
    },
    "calculated": {
      "muzzleVelocityMps": 715,
      "muzzleEnergyJ": 2037,
      "effectiveRangeM": 414,
      "terminalBallistics": {
        "tkoFactor": 12.9,
        "momentumKgMs": 5.7,
        "sectionalDensity": 0.181,
        "hatcherRSP": 103,
        "energyDensityJCm2": 4135.2
      }
    },
    "sourceReported": {
      "muzzleVelocityMps": 710,
      "muzzleEnergyJ": 2010,
      "effectiveRangeM": 300
    },
    "zeroing": {
      "zeroM": 25,
      "sightHeightMm": 20
    },
    "trajectory": [
      {
        "distanceM": 100,
        "velocityMps": 686,
        "energyJ": 1873,
        "dropCm": 10,
        "pathCm": -3.2,
        "timeOfFlightS": 0.143,
        "machNumber": 2,
        "momentumKgMs": 5.46,
        "tkoFactor": 12.3,
        "energyDensityJCm2": 3801.5
      }
    ],
    "error": null
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/load?ammo_id=m882' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getSchematics()

`GET /v1/firearms/{id}/schematics`

Auth: API key required  
Tier: Studio+

Get the technical schematics for a firearm. Returns exploded-view and cutaway drawings with their attribution.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Firearm slug |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | FirearmSchematic[] | The response payload. |
| `data[].id` | integer | Autoincrementing schematic id. |
| `data[].firearmId` | string | Slug of the firearm this row belongs to. |
| `data[].title` | string | What the document is called. |
| `data[].type` | SchematicType | What a schematic document is. |
| `data[].url` | string | Absolute, directly fetchable URL. |
| `data[].format` | string \| null | File format, e.g. `pdf`. |
| `data[].version` | string \| null | Revision of the document, where the publisher versions it. |
| `data[].manufacturer` | string \| null | Publisher of the document, often but not always the maker. |
| `data[].source` | string \| null | Where the document came from. |
| `data[].sourceUrl` | string \| null | Page the asset or document was taken from. |
| `data[].author` | string \| null | Who made the asset, where it is credited. |
| `data[].license` | string \| null | Licence the asset is held under, where one is recorded. |
| `data[].createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |

```json
{
  "success": true,
  "data": [
    {
      "id": 12,
      "firearmId": "glock-g17-gen5",
      "title": "Exploded view",
      "type": "blueprint",
      "url": "https://assets.gunspec.io/firearms/schematics/glock-g17-gen5-exploded.pdf",
      "format": "pdf",
      "version": null,
      "manufacturer": "Glock Ges.m.b.H.",
      "source": null,
      "sourceUrl": null,
      "author": null,
      "license": null,
      "createdAt": "2026-05-26 12:36:27"
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/daniel-defense-mk18-riii/schematics' \
  --header 'X-API-Key: your_key'
```

## client.firearms.popular()

`GET /v1/popular/firearms`

Auth: API key required  
Tier: Explorer+

List the most viewed firearms over a trailing window.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `days` | integer | no | Trailing window in days |
| `limit` | integer | no | How many to return |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | PopularFirearm[] | The response payload. |
| `data[].entity_id` | string | Slug of the firearm. Named `entity_id` because the rollup counts views of any entity kind; it is `id` on the catalog endpoints. |
| `data[].entity_name` | string | Display name of the firearm. |
| `data[].manufacturer_id` | string \| null | Slug of the manufacturer. `manufacturerId` on the camelCase endpoints. |
| `data[].category_id` | string \| null | Slug of the category. `categoryId` on the camelCase endpoints. |
| `data[].country_of_origin` | string \| null | ISO 3166-1 alpha-2 code of the country of origin. |
| `data[].year_introduced` | integer \| null | Year the model was introduced. |
| `data[].status` | FirearmStatus \| null | 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. |
| `data[].svg_line_art_url` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data[].total_views` | integer | Views recorded inside the trailing window given by `days`. |
| `data[].total_unique_users` | integer | Distinct visitors behind those views. Forty views from one reader is a different fact from forty readers. |

```json
{
  "success": true,
  "data": [
    {
      "entity_id": "glock-g17-gen5",
      "entity_name": "Glock 17 Gen5",
      "manufacturer_id": "glock",
      "category_id": "pistol",
      "country_of_origin": "AT",
      "year_introduced": 2017,
      "status": "in_production",
      "svg_line_art_url": "https://api.gunspec.io/v1/firearms/glock-g17-gen5/media/silhouette",
      "total_views": 49,
      "total_unique_users": 4
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/popular/firearms?days=7&limit=10' \
  --header 'X-API-Key: your_key'
```

## client.firearms.resolve()

`GET /v1/firearms/resolve`

Auth: API key required  
Tier: Builder+

Resolve a free-text name to a firearm id. Builder.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | yes | The name to resolve, up to 200 characters. |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | What one query resolved to. |
| `data.query` | string | The query this result answers, echoed back so a batch response can be matched to its request. |
| `data.status` | string | One of `resolved` (exactly one record), `ambiguous` (several equally good, none chosen) or `not_found`. |
| `data.firearmId` | string \| null | The resolved catalog id. Null on `ambiguous` and `not_found`, since an ambiguous query is deliberately not decided for you. |
| `data.match` | string \| null | How the resolution was made, or null when nothing resolved. Never `fuzzy`: a suggestion is not a resolution. |
| `data.score` | number | Confidence in the resolution, 0 to 1. Derived from how much of the query a catalogued spelling accounted for and how strong that spelling is as evidence. |
| `data.matchedTokens` | string[] | The words of the query a catalogued spelling accounted for. |
| `data.unresolvedTokens` | string[] | The words it did not. Reported rather than guessed at: a variant we do not hold appears here instead of being silently dropped. |
| `data.alternatives` | object[] | Other records this query could mean, best first. On `ambiguous` these are the tied candidates and choosing between them is yours. |
| `data.alternatives[].firearmId` | string | The catalog id of this candidate. |
| `data.alternatives[].name` | string | The catalogued name of this candidate. |
| `data.alternatives[].manufacturerName` | string \| null | The maker, or null where the record has none. |
| `data.alternatives[].match` | string | How this candidate was matched. One of `slug`, `name`, `alternate`, `shorthand`, `base` (the name with its maker removed), `family` (the model designation with its trailing descriptor removed, e.g. `M4` from `M4 Carbine`) or `fuzzy`. |
| `data.alternatives[].matchedAlias` | string \| null | The catalogued spelling that matched, or null for a fuzzy suggestion. |
| `data.alternatives[].score` | number | Confidence in this candidate, 0 to 1. Always 0 for a fuzzy suggestion, which is offered rather than scored. |

```json
{
  "success": true,
  "data": {
    "query": "G19 gen 5 MOS",
    "status": "resolved",
    "firearmId": "glock-19-gen5",
    "match": "shorthand",
    "score": 0.6,
    "matchedTokens": [
      "g19"
    ],
    "unresolvedTokens": [
      "mos"
    ],
    "alternatives": [
      {
        "firearmId": "glock-19-gen5",
        "name": "Glock 19 Gen5",
        "manufacturerName": "Glock",
        "match": "shorthand",
        "matchedAlias": "G19 Gen5",
        "score": 0.6
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/resolve?q=G19%20gen%205%20MOS' \
  --header 'X-API-Key: your_key'
```

## client.firearms.resolveMany()

`POST /v1/firearms/resolve`

Auth: API key required  
Tier: Studio+

Resolve many names in one request. Studio.

### Request body (application/json)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `queries` | string[] | yes | The names to resolve. Between 1 and 50, each up to 200 characters. |

```json
{
  "queries": [
    "AK-47"
  ]
}
```

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.results` | object[] | One result per query, in the order the queries were sent. |
| `data.results[].query` | string | The query this result answers, echoed back so a batch response can be matched to its request. |
| `data.results[].status` | string | One of `resolved` (exactly one record), `ambiguous` (several equally good, none chosen) or `not_found`. |
| `data.results[].firearmId` | string \| null | The resolved catalog id. Null on `ambiguous` and `not_found`, since an ambiguous query is deliberately not decided for you. |
| `data.results[].match` | string \| null | How the resolution was made, or null when nothing resolved. Never `fuzzy`: a suggestion is not a resolution. |
| `data.results[].score` | number | Confidence in the resolution, 0 to 1. Derived from how much of the query a catalogued spelling accounted for and how strong that spelling is as evidence. |
| `data.results[].matchedTokens` | string[] | The words of the query a catalogued spelling accounted for. |
| `data.results[].unresolvedTokens` | string[] | The words it did not. Reported rather than guessed at: a variant we do not hold appears here instead of being silently dropped. |
| `data.results[].alternatives` | object[] | Other records this query could mean, best first. On `ambiguous` these are the tied candidates and choosing between them is yours. |

```json
{
  "success": true,
  "data": {
    "results": [
      {
        "query": "G19 gen 5 MOS",
        "status": "resolved",
        "firearmId": "glock-19-gen5",
        "match": "shorthand",
        "score": 0.6,
        "matchedTokens": [
          "g19"
        ],
        "unresolvedTokens": [
          "mos"
        ],
        "alternatives": [
          {
            "firearmId": "glock-19-gen5",
            "name": "Glock 19 Gen5",
            "manufacturerName": "Glock",
            "match": "shorthand",
            "matchedAlias": "G19 Gen5",
            "score": 0.6
          }
        ]
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 413 | `PAYLOAD_TOO_LARGE` | Request body too large |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request POST \
  --url 'https://api.gunspec.io/v1/firearms/resolve' \
  --header 'X-API-Key: your_key' \
  --header 'Content-Type: application/json' \
  --data '
{
  "queries": [
    "AK-47"
  ]
}
'
```

## client.firearms.mediaCatalog()

`GET /v1/firearms/media`

Auth: API key required  
Tier: Explorer+

Every firearm that has imagery, one row each.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | no | Page number |
| `per_page` | integer | no | Firearms per page |
| `kind` | string | no | Restrict the images on each row to one kind |
| `ids` | string | no | Only these firearms, as a comma-separated list of ids (at most 24). A firearm with no imagery is left out of the answer. |
| `If-None-Match` | string | no | The `ETag` from a previous response. If the body would be byte-identical, the API answers `304 Not Modified` with no body. A 304 counts toward the per-minute rate limit but not the daily request allowance. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | MediaCatalogEntry[] | One firearm and the real imagery it has. Deliberately three fields: this is the index for a media grid or a mirroring job, not a catalogue row. Silhouettes are excluded: every firearm has one, and they have their own build-time manifest. |
| `data[].id` | string | Slug of the firearm this row is about. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].images` | InlineMediaItem[] | The imagery on file for it, silhouette excluded, since every firearm has one of those. |
| `data[].images[].id` | integer | Autoincrementing image id. |
| `data[].images[].url` | string | Absolute, directly fetchable URL. |
| `data[].images[].kind` | MediaKind | What kind of asset a media row is. |
| `data[].images[].alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data[].images[].width` | integer \| null | Pixel width, where it is known. |
| `data[].images[].height` | integer \| null | Pixel height, where it is known. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "ak-47",
      "name": "AK-47",
      "images": [
        {
          "id": 2572,
          "url": "https://api.gunspec.io/v1/firearms/ak-47/images/2572",
          "kind": "silhouette",
          "alt": "AK-47 right profile",
          "width": 1536,
          "height": 1024
        }
      ]
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 8630,
    "totalPages": 432
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/media' \
  --header 'X-API-Key: your_key'
```

## client.firearms.listMedia()

`GET /v1/firearms/{id}/media`

Auth: API key required  
Tier: Explorer+

Every asset on a firearm, with credit and derivative sizes.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string | no | Return only one kind of asset |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | MediaItem[] | The response payload. |
| `data[].id` | integer \| null | Null for assets stored as a column rather than a row (the 3D model). |
| `data[].kind` | MediaItemKind | What kind of asset a media listing item is: a stored kind, or `feature_icon`, the drawn icon for one of the firearm's features. |
| `data[].feature` | string | On a `feature_icon` only: the tag in the firearm's `features` this drawing illustrates, exactly as the record carries it. |
| `data[].mimeType` | string \| null | Media type of the bytes, e.g. `image/png`. |
| `data[].width` | integer \| null | Pixel width, where it is known. |
| `data[].height` | integer \| null | Pixel height, where it is known. |
| `data[].alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data[].url` | string | Absolute, directly fetchable URL. |
| `data[].sizes` | object | Per-size URLs. Absent on assets we link rather than host. |
| `data[].sizes.full` | string | URL of the asset at full size. |
| `data[].sizes.display` | string | URL of the mid-size rendition, for a detail page. |
| `data[].sizes.thumb` | string | URL of the thumbnail rendition, for a list or grid. |
| `data[].credit` | object | Attribution required to reuse the asset. Populated wherever the catalogue records it. |
| `data[].credit.source` | string \| null | Where the record came from: a curated entry, an inference, or an import. |
| `data[].credit.author` | string \| null | Who made the asset, where it is credited. |
| `data[].credit.sourceUrl` | string \| null | Page the asset or document was taken from. |
| `data[].credit.license` | string \| null | Licence the asset is held under, where one is recorded. |

```json
{
  "success": true,
  "data": [
    {
      "id": 2572,
      "kind": "silhouette",
      "feature": "Picatinny rail",
      "mimeType": "image/png",
      "width": 1024,
      "height": 1024,
      "alt": "AK-47, left profile",
      "url": "https://api.gunspec.io/v1/firearms/ak-47/media/2572",
      "sizes": {
        "full": "string",
        "display": "string",
        "thumb": "string"
      },
      "credit": {
        "source": "Wikimedia Commons",
        "author": "Askild Antonsen",
        "sourceUrl": null,
        "license": "CC BY 2.0"
      }
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/media?kind=feature_icon' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getMedia()

`GET /v1/firearms/{id}/media/{selector}`

Auth: API key required  
Tier: Builder+

Metadata for one asset, by kind or by row id. Builder.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |
| `selector` | unknown | yes | Which asset: a **kind**, which resolves to the primary asset of that kind, or the numeric `id` of one row from `/v1/firearms/{id}/media`. Addressing by kind means never having to list first just to learn an id. |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `format` | string | no | Bytes, an inline data URI, or metadata only |
| `size` | string | no | WebP by default: `display` is full size with transparency, `thumb` a small preview, `full` the uploaded file (usually PNG). A missing derivative falls back to the original |
| `stroke_width` | integer | no | Silhouettes only: inject a stroke so line art reads on any background |
| `stroke_color` | string | no | Silhouettes only: stroke colour. Any CSS colour the browser understands; quotes, angle brackets, ampersands and backslashes are refused, because the value is written into an SVG attribute |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | MediaItem | One asset belonging to a firearm. Every kind, whether silhouette, render, photo, schematic or 3D model, is returned in this same shape, and `url` is always absolute and directly fetchable. |
| `data.id` | integer \| null | Null for assets stored as a column rather than a row (the 3D model). |
| `data.kind` | MediaItemKind | What kind of asset a media listing item is: a stored kind, or `feature_icon`, the drawn icon for one of the firearm's features. |
| `data.feature` | string | On a `feature_icon` only: the tag in the firearm's `features` this drawing illustrates, exactly as the record carries it. |
| `data.mimeType` | string \| null | Media type of the bytes, e.g. `image/png`. |
| `data.width` | integer \| null | Pixel width, where it is known. |
| `data.height` | integer \| null | Pixel height, where it is known. |
| `data.alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data.url` | string | Absolute, directly fetchable URL. |
| `data.sizes` | object | Per-size URLs. Absent on assets we link rather than host. |
| `data.sizes.full` | string | URL of the asset at full size. |
| `data.sizes.display` | string | URL of the mid-size rendition, for a detail page. |
| `data.sizes.thumb` | string | URL of the thumbnail rendition, for a list or grid. |
| `data.credit` | object | Attribution required to reuse the asset. Populated wherever the catalogue records it. |
| `data.credit.source` | string \| null | Where the record came from: a curated entry, an inference, or an import. |
| `data.credit.author` | string \| null | Who made the asset, where it is credited. |
| `data.credit.sourceUrl` | string \| null | Page the asset or document was taken from. |
| `data.credit.license` | string \| null | Licence the asset is held under, where one is recorded. |

```json
{
  "success": true,
  "data": {
    "id": 2572,
    "kind": "silhouette",
    "feature": "Picatinny rail",
    "mimeType": "image/png",
    "width": 1024,
    "height": 1024,
    "alt": "AK-47, left profile",
    "url": "https://api.gunspec.io/v1/firearms/ak-47/media/2572",
    "sizes": {
      "full": "string",
      "display": "string",
      "thumb": "string"
    },
    "credit": {
      "source": "Wikimedia Commons",
      "author": "Askild Antonsen",
      "sourceUrl": null,
      "license": "CC BY 2.0"
    }
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/media/silhouette' \
  --header 'X-API-Key: your_key'
```

## client.firearms.downloadMedia()

`GET /v1/firearms/{id}/media/{selector}`

Auth: API key required  
Tier: Builder+

The bytes of one asset, following the redirect to the CDN. Builder.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |
| `selector` | unknown | yes | Which asset: a **kind**, which resolves to the primary asset of that kind, or the numeric `id` of one row from `/v1/firearms/{id}/media`. Addressing by kind means never having to list first just to learn an id. |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `format` | string | no | Bytes, an inline data URI, or metadata only |
| `size` | string | no | WebP by default: `display` is full size with transparency, `thumb` a small preview, `full` the uploaded file (usually PNG). A missing derivative falls back to the original |
| `stroke_width` | integer | no | Silhouettes only: inject a stroke so line art reads on any background |
| `stroke_color` | string | no | Silhouettes only: stroke colour. Any CSS colour the browser understands; quotes, angle brackets, ampersands and backslashes are refused, because the value is written into an SVG attribute |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | MediaItem | One asset belonging to a firearm. Every kind, whether silhouette, render, photo, schematic or 3D model, is returned in this same shape, and `url` is always absolute and directly fetchable. |
| `data.id` | integer \| null | Null for assets stored as a column rather than a row (the 3D model). |
| `data.kind` | MediaItemKind | What kind of asset a media listing item is: a stored kind, or `feature_icon`, the drawn icon for one of the firearm's features. |
| `data.feature` | string | On a `feature_icon` only: the tag in the firearm's `features` this drawing illustrates, exactly as the record carries it. |
| `data.mimeType` | string \| null | Media type of the bytes, e.g. `image/png`. |
| `data.width` | integer \| null | Pixel width, where it is known. |
| `data.height` | integer \| null | Pixel height, where it is known. |
| `data.alt` | string \| null | Alt text, where one has been written. Null otherwise, so do not render an empty string. |
| `data.url` | string | Absolute, directly fetchable URL. |
| `data.sizes` | object | Per-size URLs. Absent on assets we link rather than host. |
| `data.sizes.full` | string | URL of the asset at full size. |
| `data.sizes.display` | string | URL of the mid-size rendition, for a detail page. |
| `data.sizes.thumb` | string | URL of the thumbnail rendition, for a list or grid. |
| `data.credit` | object | Attribution required to reuse the asset. Populated wherever the catalogue records it. |
| `data.credit.source` | string \| null | Where the record came from: a curated entry, an inference, or an import. |
| `data.credit.author` | string \| null | Who made the asset, where it is credited. |
| `data.credit.sourceUrl` | string \| null | Page the asset or document was taken from. |
| `data.credit.license` | string \| null | Licence the asset is held under, where one is recorded. |

```json
{
  "success": true,
  "data": {
    "id": 2572,
    "kind": "silhouette",
    "feature": "Picatinny rail",
    "mimeType": "image/png",
    "width": 1024,
    "height": 1024,
    "alt": "AK-47, left profile",
    "url": "https://api.gunspec.io/v1/firearms/ak-47/media/2572",
    "sizes": {
      "full": "string",
      "display": "string",
      "thumb": "string"
    },
    "credit": {
      "source": "Wikimedia Commons",
      "author": "Askild Antonsen",
      "sourceUrl": null,
      "license": "CC BY 2.0"
    }
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/media/silhouette' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getImageAsset()

`GET /v1/firearms/{id}/images/{imageId}`

Auth: API key required  
Tier: Builder+

One image as a base64 data URI. Builder. **Ask for a variant.** A data URI is capped at 256 KB and a catalogue original is routinely five times that, so the default request fails on exactly the images most worth embedding. `thumb` and `display` are generated derivatives and always fit; `original` is for the cases where you know the file is small. For the full-size bytes use downloadMedia, which redirects to the CDN and has no such cap.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Resource slug ID |
| `imageId` | string | yes | Image row id from /v1/firearms/{id}/images |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `format` | string | no | raw file, or a base64 data URI |
| `variant` | string | no | WebP by default: `display` is full size with transparency, `thumb` a small preview, `original` the uploaded file (usually PNG). A missing derivative falls back to the original |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.id` | string | Slug of the firearm the image belongs to. |
| `data.imageId` | integer | Numeric id of the image within that firearm image set. |
| `data.variant` | string | Size rendered: `original`, `display` or `thumb`. |
| `data.format` | string | Encoding of the returned image. `datauri` is the only format this route emits. |
| `data.mimeType` | string | Media type of the encoded bytes. |
| `data.dataUri` | string | The image inlined as an RFC 2397 data URI, ready to use as an `<img src>` with no second request. |

```json
{
  "success": true,
  "data": {
    "id": "glock-g17-gen5",
    "imageId": 6563,
    "variant": "thumb",
    "format": "datauri",
    "mimeType": "image/webp",
    "dataUri": "data:image/webp;base64,UklGRi..."
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/glock-g17-gen5/images/6563' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getModel()

`GET /v1/firearms/{id}/model`

Auth: API key required  
Tier: Builder+

The 3D model as GLB bytes. Builder.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Firearm slug |

### Response 200 (model/gltf-binary)

```json
"string"
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/ak-47/model' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getOffers()

`GET /v1/firearms/{id}/offers`

Auth: API key required  
Tier: Explorer+

Sellers stocking this firearm; link out through `client.vendor.clickUrl(offer.clickId)`.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Firearm slug |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `region` | string | no | ISO 3166-1 alpha-2 code; hides sellers who do not ship there |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | PublicOffer[] | The response payload. |
| `data[].vendor` | object | The shop stocking the part. |
| `data[].vendor.id` | string | Slug of the shop. |
| `data[].vendor.name` | string | Display name, as the maker writes it. |
| `data[].vendor.website` | string \| null | Official website, where one is known. |
| `data[].vendor.logoUrl` | string \| null | Absolute URL of the logo, or null where none is on file. |
| `data[].vendor.verifiedDomain` | string \| null | The domain the shop has proved it controls, by a DNS record or a file on the site, re-checked daily. Null when it has not, which is not a sign the shop is untrustworthy: most have simply not done it yet. |
| `data[].sku` | string | The shop's own catalogue key for the listing. A seller's identifier, not ours: it is what `PATCH /v1/vendor/offers/{sku}` addresses. |
| `data[].priceCents` | integer | Price in integer minor units of `currency`, so cents for AUD. Never a float. Format with `Intl.NumberFormat` at render rather than dividing first. |
| `data[].currency` | string | ISO 4217 code the price is in, e.g. `AUD`. Prices are integer minor units; format with `Intl.NumberFormat` at render rather than dividing first. |
| `data[].url` | string | Absolute, directly fetchable URL. |
| `data[].inStock` | boolean | Whether the shop reports it in stock. Derived from `stockQty` where the shop tracks a count. |
| `data[].clickId` | string \| null | Opaque token for the outbound link: send readers to `/v1/out/{clickId}` so the visit is counted for the seller. Null on a listing that cannot currently be followed. |
| `data[].region` | string \| null | ISO 3166-1 alpha-2 code the seller ships to. Null means they state no restriction. |
| `data[].updatedAt` | string | When the seller last refreshed this listing. A listing not refreshed inside its freshness window is hidden rather than shown stale. |

```json
{
  "success": true,
  "data": [
    {
      "vendor": {
        "id": "example-optics",
        "name": "Example Optics",
        "website": "https://example-optics.com",
        "logoUrl": "https://example-optics.com/logo.svg",
        "verifiedDomain": "example-optics.com"
      },
      "sku": "AIM-ACRO-P2",
      "priceCents": 129900,
      "currency": "AUD",
      "url": "https://example-optics.com/products/aimpoint-acro-p-2",
      "inStock": true,
      "clickId": "a1b2c3d4e5f60718",
      "region": "AU",
      "updatedAt": "2026-09-08 04:11:00"
    }
  ]
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/ak-74m/offers' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getInterfaces()

`GET /v1/firearms/{id}/interfaces`

Auth: API key required  
Tier: Studio+

The mount interfaces a firearm exposes. Studio.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Firearm slug |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.firearm` | object | A firearm, identified by slug and name. |
| `data.firearm.id` | string | Slug of the firearm. |
| `data.firearm.name` | string | Display name, as the maker writes it. |
| `data.platforms` | object[] | Platforms this firearm belongs to, whose interfaces it inherits. Reached through its own memberships and its parent chain. |
| `data.platforms[].id` | string | Slug of the platform. |
| `data.platforms[].name` | string | Display name, as the maker writes it. |
| `data.interfaces` | ResolvedInterface[] | The mount interfaces resolved for this firearm, own rows first, then inherited. |
| `data.interfaces[].position` | string | Where on the firearm the interface sits: `muzzle`, `rear`, `top`, `bottom`. |
| `data.interfaces[].standardId` | string | Id of the interface standard, kind-prefixed, e.g. `thread:1/2x28`. Ids are never renamed; new spellings become aliases. |
| `data.interfaces[].name` | string | Display name, as the maker writes it. |
| `data.interfaces[].kind` | string | What sort of interface it is: `thread`, `rail`, `mag`, `stock_mount`, `optic_plate`. Not a media kind: the shared registry filled this with `silhouette`/`render` until the consistency check caught it. |
| `data.interfaces[].source` | InterfaceSource | Where an interface row on a firearm came from. `curated` is a person; `inferred` is the backfill, capped at 0.6 confidence; the inherited kinds came through the parent chain or a platform. |
| `data.interfaces[].confidence` | number | How well evidenced this row is, 0 to 1. **A minimum, not an average**: an inferred row caps every fit computed through it, and `min_confidence` on the endpoint hides rows below a threshold. |
| `data.interfaces[].inheritedFrom` | string \| null | The parent firearm or platform this interface came from, or null when the firearm declares it itself. |
| `data.interfaces[].notes` | string \| null | Free-text notes that do not belong in a specific field. |

```json
{
  "success": true,
  "data": {
    "firearm": {
      "id": "ak-74m",
      "name": "AK-74M"
    },
    "platforms": [
      {
        "id": "ak-100",
        "name": "AK-100 series (AK-74M pattern)"
      }
    ],
    "interfaces": [
      {
        "position": "muzzle",
        "standardId": "thread:m24x1.5-rh",
        "name": "M24x1.5 right hand (AK-74, AK-100 series)",
        "kind": "thread",
        "source": "curated",
        "confidence": 1,
        "inheritedFrom": null,
        "notes": null
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/ak-74m/interfaces' \
  --header 'X-API-Key: your_key'
```

## client.firearms.getAttachments()

`GET /v1/firearms/{id}/attachments`

Auth: API key required  
Tier: Studio+

Attachments that fit, grouped by category, with the evidence for each. Studio.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Firearm slug |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `category` | string | no | Attachment category |
| `manufacturer` | string | no | Manufacturer slug |
| `include` | string | no | `compatible` (default) or `all` to include blocked candidates with `blockedBy` |
| `min_confidence` | number | no | Hide fits computed through interfaces below this confidence |
| `vendor` | string | no | Restrict to one seller. `me` resolves to the vendor your key belongs to |
| `only_offered` | boolean | no | Drop anything that seller does not currently stock. Needs a resolvable `vendor`, and yields nothing without one |
| `with_offers` | boolean | no | Attach each item's live offers under `offers`, keyed by attachment id, so price and stock render beside fitment in one call |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.firearm` | object | A firearm, identified by slug and name. |
| `data.firearm.id` | string | Slug of the firearm. |
| `data.firearm.name` | string | Display name, as the maker writes it. |
| `data.platforms` | object[] | Platforms this firearm belongs to, whose interfaces it inherits. |
| `data.platforms[].id` | string | Slug of the platform. |
| `data.platforms[].name` | string | Display name, as the maker writes it. |
| `data.interfaces` | ResolvedInterface[] | The mount interfaces resolved for this firearm, own rows first, then inherited. |
| `data.interfaces[].position` | string | Where on the firearm the interface sits: `muzzle`, `rear`, `top`, `bottom`. |
| `data.interfaces[].standardId` | string | Id of the interface standard, kind-prefixed, e.g. `thread:1/2x28`. Ids are never renamed; new spellings become aliases. |
| `data.interfaces[].name` | string | Display name, as the maker writes it. |
| `data.interfaces[].kind` | string | What sort of interface it is: `thread`, `rail`, `mag`, `stock_mount`, `optic_plate`. Not a media kind: the shared registry filled this with `silhouette`/`render` until the consistency check caught it. |
| `data.interfaces[].source` | InterfaceSource | Where an interface row on a firearm came from. `curated` is a person; `inferred` is the backfill, capped at 0.6 confidence; the inherited kinds came through the parent chain or a platform. |
| `data.interfaces[].confidence` | number | How well evidenced this row is, 0 to 1. **A minimum, not an average**: an inferred row caps every fit computed through it, and `min_confidence` on the endpoint hides rows below a threshold. |
| `data.interfaces[].inheritedFrom` | string \| null | The parent firearm or platform this interface came from, or null when the firearm declares it itself. |
| `data.interfaces[].notes` | string \| null | Free-text notes that do not belong in a specific field. |
| `data.groups` | object[] | Fitting attachments grouped by category, so a builder UI renders one panel per slot. |
| `data.groups[].category` | string | Attachment category this group holds. |
| `data.groups[].items` | AttachmentFit[] | The rows this response carries. |
| `data.counts` | object | Fitting attachments per category |
| `data.offers` | object \| null | With `with_offers=true`, live offers keyed by attachment id, the same rows `/v1/attachments/{id}/offers` returns; null when not requested. |
| `data.total` | integer | Total records matching the query across every page. |

```json
{
  "success": true,
  "data": {
    "firearm": {
      "id": "ak-74m",
      "name": "AK-74M"
    },
    "platforms": [
      {
        "id": "ak-100",
        "name": "AK-100 series (AK-74M pattern)"
      }
    ],
    "interfaces": [
      {
        "position": "muzzle",
        "standardId": "thread:m24x1.5-rh",
        "name": "M24x1.5 right hand (AK-74, AK-100 series)",
        "kind": "thread",
        "source": "curated",
        "confidence": 1,
        "inheritedFrom": null,
        "notes": null
      }
    ],
    "groups": [
      {
        "category": "stock",
        "items": [
          {
            "id": "magpul-zhukov-s-stock",
            "name": "Zhukov-S Stock",
            "category": "stock",
            "manufacturer": {
              "id": "magpul",
              "name": "Magpul Industries"
            },
            "modelNumber": "MAG585",
            "weightG": 490,
            "lengthMm": 240,
            "capacity": null,
            "boreDiameterMm": null,
            "requiresPosition": null,
            "isFactoryPart": false,
            "factoryFor": null,
            "statMods": {
              "mobility": 3,
              "recoil_control": 2
            },
            "specDeltas": {
              "folded_length_mm": 705
            },
            "imageUrl": "https://assets.gunspec.io/content/attachments/optic/aimpoint-acro-p-2/01-product.webp",
            "status": "in_production",
            "dataConfidence": 0.9,
            "updatedAt": "2026-09-07 12:56:03",
            "version": "e27e61b4b373f5a621d6ae5c67d521096131f2cf56300a1b2c3d4e5f6a7b8c9d",
            "provenance": {
              "sources": [
                "https://www.glock.com/en/products/pistols/g17-gen5"
              ],
              "sourceKinds": [
                {
                  "url": "https://www.glock.com/en/products/pistols/g17-gen5",
                  "kind": "manufacturer",
                  "archivedFrom": null,
                  "rank": 0,
                  "tier": "authority",
                  "basis": "declared",
                  "evidence": null
                }
              ],
              "bestSourceKind": "manufacturer",
              "makerSource": "https://www.glock.com/en/products/pistols/g17-gen5",
              "evidence": "maker",
              "checks": {
                "raised": 3,
                "open": 0,
                "history": [
                  {
                    "task": "verify:firearms:glock-g17-gen5",
                    "number": 1204,
                    "check": "quality:firearm-weight-fits-its-size",
                    "kind": "verify",
                    "agent": "redback",
                    "worker": "redback-046535",
                    "status": "resolved",
                    "decidedAt": "2026-09-25 07:42:39",
                    "proposed": "Read https://us.glock.com/en/pistols/g17-gen5: agrees on barrel_length, weight.",
                    "fields": [
                      "barrel_length_mm",
                      "weight_empty_g"
                    ],
                    "sources": [
                      "https://us.glock.com/en/pistols/g17-gen5"
                    ],
                    "review": {
                      "decision": "accepted",
                      "at": null
                    },
                    "finding": {
                      "why": "2.22 g/cm3 against a submachine-gun / carbon-steel median of 0.64; check the weight and the dimensions",
                      "evidence": {
                        "apparentDensity": 2.218,
                        "categoryMedian": 0.635
                      }
                    }
                  }
                ]
              },
              "dataConfidence": 0.95,
              "verifiedAt": "2026-09-08",
              "verifiedFields": [
                "barrel_length_mm",
                "weight_empty_g"
              ],
              "specSource": null,
              "updatedAt": "2026-09-08 04:10:22",
              "version": "ba5c1d9e06c279a5"
            },
            "provides": [],
            "fits": true,
            "fitType": "direct",
            "source": "curated",
            "via": [
              {
                "position": "rear",
                "standardId": "stock:ak-100-folder",
                "source": "curated",
                "adapterId": null
              }
            ],
            "adapter": {
              "id": "rs-regulate-ak-303m",
              "name": "RS Regulate AK-303M"
            },
            "confidence": 1,
            "reason": "Mounts on AK-100 series polymer side-folder trunnion (rear)",
            "blockedBy": null,
            "routes": [
              {
                "fitType": "adapter",
                "adapter": {
                  "id": "rs-regulate-ak-303m",
                  "name": "RS Regulate AK-303M"
                },
                "via": [
                  {
                    "position": "rear",
                    "standardId": "stock:ak-100-folder",
                    "source": "curated",
                    "adapterId": null
                  }
                ],
                "confidence": 1,
                "source": "curated",
                "adapterConfidence": 0.95,
                "caveats": [
                  "Needs 30 mm rings, not listed here"
                ]
              }
            ],
            "caveats": []
          }
        ]
      }
    ],
    "counts": {
      "stock": 4,
      "optic": 12,
      "suppressor": 1
    },
    "offers": null,
    "total": 42
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/firearms/ak-74m/attachments' \
  --header 'X-API-Key: your_key'
```
