# Ammunition

Query individual ammunition loads, retrieve ballistic trajectory tables, calculate firearm-specific performance, and load firearms with full ballistic profiles. Essential for ballistic simulation, ammunition databases, and game development.

Source: https://docs.gunspec.io/en/api/ammunition

## Calculate Firearm Ballistics

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

Auth: API key required  
Tier: Builder+

Computes muzzle performance for a firearm using its own barrel length and a chosen ammunition load, and compares the result against the figures the catalog records for the firearm.

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

### Notes

- Requires **Builder** tier or higher.
- Calculation uses the firearm barrel length to adjust the ammunition reference velocity using a barrel length velocity model.
- Delta values show how much the calculated values differ from manufacturer-reported specifications. Small deltas indicate good model accuracy.

### 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'
```

## Load Firearm

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

Auth: API key required  
Tier: Builder+

Returns the full ballistic profile for a firearm chambered with a given load: muzzle velocity and energy, effective range, terminal indices, and the trajectory sampled from the muzzle out to 1000 m.

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

### Notes

- Requires **Builder** tier or higher.
- When ammo_id is omitted, the endpoint auto-resolves ammunition in this order: (1) the firearm's default ammunition, (2) the most common load for the caliber.
- Muzzle velocity is adjusted based on the firearm's barrel length relative to the ammunition's reference barrel.
- The trajectory table covers distances from 0 to the effective range at regular intervals.
- Ideal for game engines that need a ready-to-use ballistic profile for a weapon.

### 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'
```

## Ammunition by Caliber

`GET /v1/calibers/{id}/ammunition`

Auth: API key required  
Tier: Builder+

Returns the specific factory loads available in a given cartridge.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Caliber 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` | AmmunitionLoad[] | A specific factory ammunition load within a caliber. |
| `data[].id` | string | URL-safe slug identifying the load, e.g. `9mm-124gr-fmj`. |
| `data[].caliberId` | string | Slug of the cartridge. Resolve against `/v1/calibers/{id}`. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].designation` | string \| null | Military or standards designation for the load, e.g. `M882`. |
| `data[].manufacturer` | string \| null | Who makes it. |
| `data[].countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data[].bulletWeightG` | number | Bullet weight in grams. Divide by 0.0648 for grains. |
| `data[].bulletType` | string | Bullet construction: `fmj`, `jhp`, `sp`, `match`. |
| `data[].ballisticCoefficientG1` | number \| null | Ballistic coefficient against the G1 drag model, the convention for flat-based bullets. |
| `data[].ballisticCoefficientG7` | number \| null | Ballistic coefficient against the G7 drag model, which fits boat-tail rifle bullets better than G1. |
| `data[].referenceVelocityMps` | number | Muzzle velocity in metres per second **at the reference barrel length below**, not at the barrel of any particular firearm. |
| `data[].referenceBarrelLengthMm` | number | Barrel length in millimetres the reference velocity was measured from. Velocity for another barrel is extrapolated from this pair. |
| `data[].sectionalDensity` | number \| null | Bullet mass divided by the square of its diameter. Higher penetrates better, all else equal. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].isCommon` | integer | 1 when the load is commonly stocked. |
| `data[].bulletSvgUrl` | string \| null | Absolute URL of the rendered bullet diagram. |
| `data[].velocityRetentionExponent` | number \| null | Decay exponent used when extrapolating velocity to a barrel length other than the reference one. |
| `data[].description` | string \| null | Prose summary of the record, where one has been written. |
| `data[].isMilitary` | integer | 1 when the load is a military issue round. |
| `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 figure and does not move for a re-run of the seed. Before migration 0113 the seed rewrote every load on every run, and this said so. |
| `data[].sources` | string[] | Where the figures came from. Null on every load today: this family has somewhere to cite from and nothing cited yet, which the data quality page counts as a gap rather than hiding. Check a velocity against the maker until it is filled. |
| `data[].dataConfidence` | number \| null | How well specified the load is, on the same 0 to 1 scale as a firearm's. Null until there are sources behind it: a confidence figure with nothing supporting it looks like evidence and is not. |
| `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[].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": "m882",
      "caliberId": "9x19mm-parabellum",
      "name": "9mm 124gr FMJ",
      "designation": "M882",
      "manufacturer": "Federal",
      "countryOfOrigin": "US",
      "bulletWeightG": 8.03,
      "bulletType": "fmj",
      "ballisticCoefficientG1": 0.165,
      "ballisticCoefficientG7": null,
      "referenceVelocityMps": 360,
      "referenceBarrelLengthMm": 114,
      "sectionalDensity": 0.14,
      "yearIntroduced": 1985,
      "isCommon": 1,
      "bulletSvgUrl": null,
      "velocityRetentionExponent": 0.12,
      "description": "Special purpose subsonic 7.62x39mm ammunition.",
      "isMilitary": 1,
      "createdAt": "2026-05-26 12:30:51",
      "updatedAt": "2026-05-26 12:30:51",
      "sources": [
        "string"
      ],
      "dataConfidence": null,
      "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"
      },
      "version": "e27e61b4b373f5a621d6ae5c67d521096131f2cf56300a1b2c3d4e5f6a7b8c9d"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 559,
    "totalPages": 28
  }
}
```

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

### Notes

- Requires **Builder** tier or higher.
- Returns all known ammunition loads for the given caliber, including military designations, commercial loads, and specialty rounds.

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/calibers/9x19mm-parabellum/ammunition' \
  --header 'X-API-Key: your_key'
```

## List Ammunition

`GET /v1/ammunition`

Auth: API key required  
Tier: Builder+

Returns a paginated list of specific ammunition loads across all calibers.

### 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) |
| `q` | string | no | Substring match on the load name, designation, maker or cartridge name |
| `caliber_id` | string | no | Filter by caliber slug |
| `bullet_type` | string | no | Filter by bullet construction |
| `is_common` | integer | no | Restrict to commonly stocked loads (1) or uncommon ones (0) |
| `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` | AmmunitionLoad[] | A specific factory ammunition load within a caliber. |
| `data[].id` | string | URL-safe slug identifying the load, e.g. `9mm-124gr-fmj`. |
| `data[].caliberId` | string | Slug of the cartridge. Resolve against `/v1/calibers/{id}`. |
| `data[].name` | string | Display name, as the maker writes it. |
| `data[].designation` | string \| null | Military or standards designation for the load, e.g. `M882`. |
| `data[].manufacturer` | string \| null | Who makes it. |
| `data[].countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data[].bulletWeightG` | number | Bullet weight in grams. Divide by 0.0648 for grains. |
| `data[].bulletType` | string | Bullet construction: `fmj`, `jhp`, `sp`, `match`. |
| `data[].ballisticCoefficientG1` | number \| null | Ballistic coefficient against the G1 drag model, the convention for flat-based bullets. |
| `data[].ballisticCoefficientG7` | number \| null | Ballistic coefficient against the G7 drag model, which fits boat-tail rifle bullets better than G1. |
| `data[].referenceVelocityMps` | number | Muzzle velocity in metres per second **at the reference barrel length below**, not at the barrel of any particular firearm. |
| `data[].referenceBarrelLengthMm` | number | Barrel length in millimetres the reference velocity was measured from. Velocity for another barrel is extrapolated from this pair. |
| `data[].sectionalDensity` | number \| null | Bullet mass divided by the square of its diameter. Higher penetrates better, all else equal. |
| `data[].yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data[].isCommon` | integer | 1 when the load is commonly stocked. |
| `data[].bulletSvgUrl` | string \| null | Absolute URL of the rendered bullet diagram. |
| `data[].velocityRetentionExponent` | number \| null | Decay exponent used when extrapolating velocity to a barrel length other than the reference one. |
| `data[].description` | string \| null | Prose summary of the record, where one has been written. |
| `data[].isMilitary` | integer | 1 when the load is a military issue round. |
| `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 figure and does not move for a re-run of the seed. Before migration 0113 the seed rewrote every load on every run, and this said so. |
| `data[].sources` | string[] | Where the figures came from. Null on every load today: this family has somewhere to cite from and nothing cited yet, which the data quality page counts as a gap rather than hiding. Check a velocity against the maker until it is filled. |
| `data[].dataConfidence` | number \| null | How well specified the load is, on the same 0 to 1 scale as a firearm's. Null until there are sources behind it: a confidence figure with nothing supporting it looks like evidence and is not. |
| `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[].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": "m882",
      "caliberId": "9x19mm-parabellum",
      "name": "9mm 124gr FMJ",
      "designation": "M882",
      "manufacturer": "Federal",
      "countryOfOrigin": "US",
      "bulletWeightG": 8.03,
      "bulletType": "fmj",
      "ballisticCoefficientG1": 0.165,
      "ballisticCoefficientG7": null,
      "referenceVelocityMps": 360,
      "referenceBarrelLengthMm": 114,
      "sectionalDensity": 0.14,
      "yearIntroduced": 1985,
      "isCommon": 1,
      "bulletSvgUrl": null,
      "velocityRetentionExponent": 0.12,
      "description": "Special purpose subsonic 7.62x39mm ammunition.",
      "isMilitary": 1,
      "createdAt": "2026-05-26 12:30:51",
      "updatedAt": "2026-05-26 12:30:51",
      "sources": [
        "string"
      ],
      "dataConfidence": null,
      "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"
      },
      "version": "e27e61b4b373f5a621d6ae5c67d521096131f2cf56300a1b2c3d4e5f6a7b8c9d"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 559,
    "totalPages": 28
  }
}
```

### 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/ammunition?caliber_id=9x19mm-parabellum&bullet_type=fmj' \
  --header 'X-API-Key: your_key'
```

## Get Ammunition

`GET /v1/ammunition/{id}`

Auth: API key required  
Tier: Builder+

Returns the full specification for a single ammunition load.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Ammunition load 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` | AmmunitionLoad | A specific factory ammunition load within a caliber. |
| `data.id` | string | URL-safe slug identifying the load, e.g. `9mm-124gr-fmj`. |
| `data.caliberId` | string | Slug of the cartridge. Resolve against `/v1/calibers/{id}`. |
| `data.name` | string | Display name, as the maker writes it. |
| `data.designation` | string \| null | Military or standards designation for the load, e.g. `M882`. |
| `data.manufacturer` | string \| null | Who makes it. |
| `data.countryOfOrigin` | string \| null | ISO 3166-1 alpha-2 code of where it was designed, e.g. `AT`. |
| `data.bulletWeightG` | number | Bullet weight in grams. Divide by 0.0648 for grains. |
| `data.bulletType` | string | Bullet construction: `fmj`, `jhp`, `sp`, `match`. |
| `data.ballisticCoefficientG1` | number \| null | Ballistic coefficient against the G1 drag model, the convention for flat-based bullets. |
| `data.ballisticCoefficientG7` | number \| null | Ballistic coefficient against the G7 drag model, which fits boat-tail rifle bullets better than G1. |
| `data.referenceVelocityMps` | number | Muzzle velocity in metres per second **at the reference barrel length below**, not at the barrel of any particular firearm. |
| `data.referenceBarrelLengthMm` | number | Barrel length in millimetres the reference velocity was measured from. Velocity for another barrel is extrapolated from this pair. |
| `data.sectionalDensity` | number \| null | Bullet mass divided by the square of its diameter. Higher penetrates better, all else equal. |
| `data.yearIntroduced` | integer \| null | Year it entered production or service. Null where the date is disputed or unknown. |
| `data.isCommon` | integer | 1 when the load is commonly stocked. |
| `data.bulletSvgUrl` | string \| null | Absolute URL of the rendered bullet diagram. |
| `data.velocityRetentionExponent` | number \| null | Decay exponent used when extrapolating velocity to a barrel length other than the reference one. |
| `data.description` | string \| null | Prose summary of the record, where one has been written. |
| `data.isMilitary` | integer | 1 when the load is a military issue round. |
| `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 figure and does not move for a re-run of the seed. Before migration 0113 the seed rewrote every load on every run, and this said so. |
| `data.sources` | string[] | Where the figures came from. Null on every load today: this family has somewhere to cite from and nothing cited yet, which the data quality page counts as a gap rather than hiding. Check a velocity against the maker until it is filled. |
| `data.dataConfidence` | number \| null | How well specified the load is, on the same 0 to 1 scale as a firearm's. Null until there are sources behind it: a confidence figure with nothing supporting it looks like evidence and is not. |
| `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.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": {
    "id": "m882",
    "caliberId": "9x19mm-parabellum",
    "name": "9mm 124gr FMJ",
    "designation": "M882",
    "manufacturer": "Federal",
    "countryOfOrigin": "US",
    "bulletWeightG": 8.03,
    "bulletType": "fmj",
    "ballisticCoefficientG1": 0.165,
    "ballisticCoefficientG7": null,
    "referenceVelocityMps": 360,
    "referenceBarrelLengthMm": 114,
    "sectionalDensity": 0.14,
    "yearIntroduced": 1985,
    "isCommon": 1,
    "bulletSvgUrl": null,
    "velocityRetentionExponent": 0.12,
    "description": "Special purpose subsonic 7.62x39mm ammunition.",
    "isMilitary": 1,
    "createdAt": "2026-05-26 12:30:51",
    "updatedAt": "2026-05-26 12:30:51",
    "sources": [
      "string"
    ],
    "dataConfidence": null,
    "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"
    },
    "version": "e27e61b4b373f5a621d6ae5c67d521096131f2cf56300a1b2c3d4e5f6a7b8c9d"
  }
}
```

### 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/ammunition/m882' \
  --header 'X-API-Key: your_key'
```

## Ammunition Ballistics

`GET /v1/ammunition/{id}/ballistics`

Auth: API key required  
Tier: Builder+

Computes velocity, energy, drop from the bore and the path from the line of sight at the requested distances for one load, optionally adjusted for a specific barrel length and sighting. Flown through the ICAO standard atmosphere at sea level with the G1 or G7 drag table its ballistic coefficient is stated against.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Ammunition load slug |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `barrel_length_mm` | number | no | Barrel length to adjust muzzle velocity for |
| `distances` | string | no | Comma-separated distances in metres |
| `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.ammunition` | object | The load the figures are for. |
| `data.ammunition.id` | string | Slug of the load the table was computed for. |
| `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.barrelLengthMm` | number \| null | Barrel length in millimetres, measured from the breech face. |
| `data.muzzleVelocityMps` | number | Muzzle velocity in metres per second, for the default load. |
| `data.muzzleEnergyJ` | number | Muzzle energy in joules, for the default load. |
| `data.effectiveRangeM` | number | Effective range in metres against a point target. |
| `data.terminalBallistics` | TerminalBallistics | Terminal performance indices computed at the muzzle. |
| `data.terminalBallistics.tkoFactor` | number | Taylor Knock-Out factor. |
| `data.terminalBallistics.momentumKgMs` | number | Momentum in kilogram-metres per second. |
| `data.terminalBallistics.sectionalDensity` | number | Bullet mass divided by the square of its diameter. Higher penetrates better, all else equal. |
| `data.terminalBallistics.hatcherRSP` | number | Hatcher Relative Stopping Power. |
| `data.terminalBallistics.energyDensityJCm2` | number | Energy per unit frontal area, in joules per square centimetre. |
| `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 solution sampled at each requested distance. |
| `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. |

```json
{
  "success": true,
  "data": {
    "ammunition": {
      "id": "m882",
      "name": "9mm 124gr FMJ",
      "caliberId": "9x19mm-parabellum"
    },
    "barrelLengthMm": 114,
    "muzzleVelocityMps": 360,
    "muzzleEnergyJ": 510,
    "effectiveRangeM": 50,
    "terminalBallistics": {
      "tkoFactor": 12.9,
      "momentumKgMs": 5.7,
      "sectionalDensity": 0.181,
      "hatcherRSP": 103,
      "energyDensityJCm2": 4135.2
    },
    "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
      }
    ]
  }
}
```

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

### Notes

- Requires **Builder** tier or higher.
- If barrel_length_mm is provided, muzzle velocity is adjusted based on barrel length relative to the reference barrel.
- Default distances are 0, 100, 200, 300, and 500 meters. Maximum 20 distance points per request.
- Trajectory calculations use a G1 drag model with standard atmospheric conditions (15C, 1013 hPa, sea level).

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/ammunition/m882/ballistics?distances=0,100,200,300,500' \
  --header 'X-API-Key: your_key'
```

## Bullet SVG Image

`GET /v1/ammunition/{id}/bullet.svg`

Auth: Optional API key  
Tier: Explorer+

Returns a scale SVG cross-section of the bullet, generated from its recorded dimensions.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Ammunition load slug |

### Response 200 (image/svg+xml)

| Field | Type | Description |
| --- | --- | --- |
| `Content-Type` | header | image/svg+xml |
| `Cache-Control` | header | public, max-age=86400, s-maxage=604800 (cached for 24h client / 7 days CDN) |

```json
<!-- Returns raw SVG markup -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98 50" width="98" height="50" role="img" aria-label="FMJ bullet">
  <!-- Copper jacket with gradient, boat-tail base, ogive nose -->
  <!-- Cannelure groove + specular highlight -->
</svg>
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- **No authentication required.** This is a public image endpoint.
- Aggressively cached: 24-hour client cache, 7-day CDN cache.
- Every ammunition list/detail response includes a `bulletSvgUrl` field with the full URL to this endpoint.
- Bullet profiles: **FMJ** (rounded ogive), **JHP** (hollow cavity), **AP** (steel penetrator tip), **SP** (exposed lead tip), **OTM** (match-grade open tip).
- SVG proportions are derived from the caliber's `bullet_diameter_mm` for realistic scaling.
- Ideal for ammunition catalogs, e-commerce product pages, game UIs, and educational material.

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/ammunition/m882/bullet.svg' \
  --header 'X-API-Key: your_key'
```
