# Attachments

Resource class for attachments and their compatibility.

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

## client.attachments.list()

`GET /v1/attachments`

Auth: API key required  
Tier: Explorer+

List attachments. `fits` and `only_offered` change what the list means: the first runs the fit engine, the second restricts to one seller's stock.

### 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) |
| `category` | string | no | Attachment category |
| `manufacturer` | string | no | Manufacturer slug |
| `fits` | string | no | Only attachments that fit this firearm |
| `requires` | string | no | Only attachments requiring this interface standard |
| `q` | string | no | Substring match on name |
| `factory` | boolean | no | Only factory parts (`true`) or only aftermarket (`false`) |
| `status` | string | no | Production status. The catalog keeps discontinued parts on purpose, so a store front asks for `in_production` |
| `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 |
| `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` | Attachment[] | One page of results |
| `data[].id` | string | URL-safe slug identifying the part, e.g. `magpul-zhukov-s-stock`. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].category` | string | What kind of part it is: `stock`, `optic`, `muzzle`, `magazine`, `grip`. |
| `data[].manufacturer` | ManufacturerRef \| null | Who makes it. |
| `data[].modelNumber` | string \| null | The maker's own SKU, where they publish one. |
| `data[].weightG` | number \| null | Weight in grams. |
| `data[].lengthMm` | number \| null | Length in millimetres. |
| `data[].capacity` | integer \| null | Magazines only |
| `data[].boreDiameterMm` | number \| null | Suppressors and muzzle devices |
| `data[].requiresPosition` | string \| null | The mount position this part occupies, or null when it takes none. |
| `data[].isFactoryPart` | boolean | True when the part ships on the firearm from the factory. A factory part carries no `statMods` or `specDeltas`, because the firearm's own figures already include it. |
| `data[].factoryFor` | string \| null | The firearm this is the factory part for, when `isFactoryPart` is true. |
| `data[].statMods` | object | Editorial adjustments to the game statistics once fitted, keyed by stat. Same status as `game_*` on a firearm: **not measured**. |
| `data[].specDeltas` | object | Specification changes once fitted, keyed by field: a folding stock changes the folded length, for example. |
| `data[].imageUrl` | string \| null | Absolute URL of the manufacturer product photo on the assets CDN, ready to render. Null when no photo has been sourced yet. |
| `data[].status` | AttachmentStatus | Production status of an attachment. `limited` corresponds to a firearm's `limited_production`. |
| `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[].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[].provides` | string[] | Standard ids this adds to the firearm once fitted. Resolved to `{ id, name, kind }` rows on `/v1/attachments/{id}`. |
| `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": "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": []
    }
  ],
  "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/attachments' \
  --header 'X-API-Key: your_key'
```

## client.attachments.get()

`GET /v1/attachments/{id}`

Auth: API key required  
Tier: Explorer+

Get one attachment with its requirements, provisions and caliber ratings.

### Path parameters

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

### 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` | AttachmentDetail | The response payload |
| `data.id` | string | URL-safe slug identifying the part, e.g. `magpul-zhukov-s-stock`. |
| `data.name` | string | Display name, as the maker writes it. |
| `data.category` | string | What kind of part it is: `stock`, `optic`, `muzzle`, `magazine`, `grip`. |
| `data.manufacturer` | ManufacturerRef \| null | Who makes it. |
| `data.modelNumber` | string \| null | The maker's own SKU, where they publish one. |
| `data.weightG` | number \| null | Weight in grams. |
| `data.lengthMm` | number \| null | Length in millimetres. |
| `data.capacity` | integer \| null | Magazines only |
| `data.boreDiameterMm` | number \| null | Suppressors and muzzle devices |
| `data.requiresPosition` | string \| null | The mount position this part occupies, or null when it takes none. |
| `data.isFactoryPart` | boolean | True when the part ships on the firearm from the factory. A factory part carries no `statMods` or `specDeltas`, because the firearm's own figures already include it. |
| `data.factoryFor` | string \| null | The firearm this is the factory part for, when `isFactoryPart` is true. |
| `data.statMods` | object | Editorial adjustments to the game statistics once fitted, keyed by stat. Same status as `game_*` on a firearm: **not measured**. |
| `data.specDeltas` | object | Specification changes once fitted, keyed by field: a folding stock changes the folded length, for example. |
| `data.imageUrl` | string \| null | Absolute URL of the manufacturer product photo on the assets CDN, ready to render. Null when no photo has been sourced yet. |
| `data.status` | AttachmentStatus | Production status of an attachment. `limited` corresponds to a firearm's `limited_production`. |
| `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.updatedAt` | string | When a field we serve last changed. |
| `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.description` | string \| null | Prose summary of the record, where one has been written. |
| `data.specs` | object | Category-specific facts |
| `data.sources` | string[] | JSON array of the pages the figures were taken from, as a string. |
| `data.sourceUrl` | string \| null | Page the asset or document was taken from. |
| `data.yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data.requires` | StandardRef[][] | Requirement groups. One standard from each group must be present on the firearm. Empty means universal fit. |
| `data.provides` | StandardRef[] | Standards this adds to the firearm once fitted, resolved to `{ id, name, kind, position }`. |
| `data.provides[].id` | string | Kind-prefixed standard id. |
| `data.provides[].name` | string | Display name, as the maker writes it. |
| `data.provides[].kind` | string | What sort of interface it is. |
| `data.provides[].position` | string \| null | Where on the firearm the standard sits once fitted, when that is not where this part mounts: a side mount clamps to the side rail and puts its Picatinny rail over the receiver. Null means where this part mounts. |
| `data.caliberRatings` | object[] | What this part is rated for, where being rated matters, meaning suppressors and magazines. The caliber, bore and minimum-barrel gate is checked against these and is never bridged or inferred. |
| `data.caliberRatings[].caliberId` | string | Slug of the cartridge. Resolve against `/v1/calibers/{id}`. |
| `data.caliberRatings[].caliberName` | string \| null | Display name of the cartridge. |
| `data.caliberRatings[].minBarrelLengthMm` | number \| null | Shortest barrel in millimetres this part is rated for in that cartridge. Null where the maker states none. |
| `data.caliberRatings[].fullAutoRated` | boolean | Whether the maker rates it for automatic fire in that cartridge. |
| `data.caliberRatings[].notes` | string \| null | Free-text notes that do not belong in a specific field. |
| `data.curatedFits` | integer | Number of firearms with a curated override for this attachment |
| `data.createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |

```json
{
  "success": true,
  "data": {
    "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 00:00:00",
    "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"
    },
    "description": null,
    "specs": {},
    "sources": [
      "string"
    ],
    "sourceUrl": null,
    "yearIntroduced": null,
    "requires": [
      [
        {
          "id": "stock:ak-100-folder",
          "name": "AK-100 series polymer side-folder trunnion (AK-74M pattern)",
          "kind": "stock_mount"
        }
      ]
    ],
    "provides": [
      {
        "id": "stock:ak-100-folder",
        "name": "AK-100 series polymer side-folder trunnion (AK-74M pattern)",
        "kind": "stock_mount",
        "position": "top"
      }
    ],
    "caliberRatings": [
      {
        "caliberId": "5-45x39mm",
        "caliberName": "5.45x39mm",
        "minBarrelLengthMm": 254,
        "fullAutoRated": false,
        "notes": null
      }
    ],
    "curatedFits": 0,
    "createdAt": "2026-09-07 00:00: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/attachments/magpul-zhukov-s-stock' \
  --header 'X-API-Key: your_key'
```

## client.attachments.getFirearms()

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

Auth: API key required  
Tier: Studio+

List the firearms an attachment fits, with how each fit was reached. Studio.

### Path parameters

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

### 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) |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object[] | One page of results |
| `data[].id` | string | Slug of the firearm the attachment fits. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].fitType` | FitType \| null | How it fits: `direct` on the firearm own interface, `adapter` through the part named in `adapter`, or `curated` from an override on the attachment. Null when it does not fit. |
| `data[].source` | FitSource \| null | The weakest evidence behind the fit. |
| `data[].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[].reason` | string | Plain-language account of why it fits, safe to show in a UI. |
| `data[].via` | FitVia[] | The interfaces that satisfied the attachment requirements, with where each came from. |
| `data[].via[].position` | string | Where on the firearm the interface sits: `muzzle`, `rear`, `top`, `bottom`. |
| `data[].via[].standardId` | string | Id of the interface standard, kind-prefixed, e.g. `thread:1/2x28`. Ids are never renamed; new spellings become aliases. |
| `data[].via[].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[].via[].adapterId` | string \| null | The adapter that bridges this fit, or null when none is needed. Threads, magazine wells and the caliber gate are never bridged. |
| `data[].adapter` | AdapterRef \| null | The adapter the fit goes through, when `fitType` is `adapter`. Null for a direct or curated fit. |
| `data[].manufacturer` | ManufacturerRef \| null | Who makes it. |
| `data[].category` | string \| null | Display name of the firearm category. |
| `data[].images` | InlineMediaItem[] | Renders and photographs of the firearm, ordered so `images[0]` is the frame to show at rest. |
| `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[].svgLineArtUrl` | string \| null | Line-art silhouette, or null where none has been drawn. |
| `data[].yearIntroduced` | integer | Year it entered production or service. Null where the date is disputed or unknown. |
| `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-74m",
      "name": "AK-74M",
      "fitType": "direct",
      "source": "curated",
      "confidence": 1,
      "reason": "Mounts on AK-100 series polymer side-folder trunnion (rear)",
      "via": [
        {
          "position": "rear",
          "standardId": "stock:ak-100-folder",
          "source": "curated",
          "adapterId": null
        }
      ],
      "adapter": {
        "id": "rs-regulate-ak-303m",
        "name": "RS Regulate AK-303M"
      },
      "manufacturer": {
        "id": "magpul",
        "name": "Magpul Industries"
      },
      "category": "Assault Rifle",
      "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
        }
      ],
      "svgLineArtUrl": "https://api.gunspec.io/v1/firearms/ak-74m/media/silhouette",
      "yearIntroduced": 1991
    }
  ],
  "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 |
| 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/attachments/magpul-zhukov-s-stock/firearms' \
  --header 'X-API-Key: your_key'
```

## client.attachments.getOffers()

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

Auth: API key required  
Tier: Explorer+

Sellers stocking this attachment. Link out through clickUrl with each offer's `clickId` so the seller can see the visit came from you.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Attachment 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/attachments/magpul-zhukov-s-stock/offers?region=AU' \
  --header 'X-API-Key: your_key'
```

## client.attachments.listAutoPaging()

`GET /v1/attachments`

Auth: API key required  
Tier: Explorer+

Iterate every attachment matching the filters, fetching pages on demand.

### 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) |
| `category` | string | no | Attachment category |
| `manufacturer` | string | no | Manufacturer slug |
| `fits` | string | no | Only attachments that fit this firearm |
| `requires` | string | no | Only attachments requiring this interface standard |
| `q` | string | no | Substring match on name |
| `factory` | boolean | no | Only factory parts (`true`) or only aftermarket (`false`) |
| `status` | string | no | Production status. The catalog keeps discontinued parts on purpose, so a store front asks for `in_production` |
| `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 |
| `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` | Attachment[] | One page of results |
| `data[].id` | string | URL-safe slug identifying the part, e.g. `magpul-zhukov-s-stock`. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].category` | string | What kind of part it is: `stock`, `optic`, `muzzle`, `magazine`, `grip`. |
| `data[].manufacturer` | ManufacturerRef \| null | Who makes it. |
| `data[].modelNumber` | string \| null | The maker's own SKU, where they publish one. |
| `data[].weightG` | number \| null | Weight in grams. |
| `data[].lengthMm` | number \| null | Length in millimetres. |
| `data[].capacity` | integer \| null | Magazines only |
| `data[].boreDiameterMm` | number \| null | Suppressors and muzzle devices |
| `data[].requiresPosition` | string \| null | The mount position this part occupies, or null when it takes none. |
| `data[].isFactoryPart` | boolean | True when the part ships on the firearm from the factory. A factory part carries no `statMods` or `specDeltas`, because the firearm's own figures already include it. |
| `data[].factoryFor` | string \| null | The firearm this is the factory part for, when `isFactoryPart` is true. |
| `data[].statMods` | object | Editorial adjustments to the game statistics once fitted, keyed by stat. Same status as `game_*` on a firearm: **not measured**. |
| `data[].specDeltas` | object | Specification changes once fitted, keyed by field: a folding stock changes the folded length, for example. |
| `data[].imageUrl` | string \| null | Absolute URL of the manufacturer product photo on the assets CDN, ready to render. Null when no photo has been sourced yet. |
| `data[].status` | AttachmentStatus | Production status of an attachment. `limited` corresponds to a firearm's `limited_production`. |
| `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[].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[].provides` | string[] | Standard ids this adds to the firearm once fitted. Resolved to `{ id, name, kind }` rows on `/v1/attachments/{id}`. |
| `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": "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": []
    }
  ],
  "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/attachments' \
  --header 'X-API-Key: your_key'
```
