# Content

Public, key-free reads for GunSpec editorial content: blog posts and the product changelog. Both return only published entries and support pagination and category filtering. Use them to embed news, release notes, or articles directly in your own app.

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

## List Changelog Entries

`GET /v1/changelog`

Auth: Optional API key  
Tier: Explorer+

Returns published product changelog entries, newest first. Also available as RSS at `/changelog.xml`.

### 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 | Filter by entry category |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | ChangelogEntry[] | One page of results |
| `data[].id` | string | Slug of the entry, taken from the file it was authored in. |
| `data[].title` | string | Entry headline. |
| `data[].body` | string | Entry body, in Markdown. |
| `data[].category` | string \| null | What kind of change: `feature`, `improvement`, `bugfix`, `data`, `breaking`. |
| `data[].publishedAt` | string | When it was published, ISO 8601. |
| `data[].published` | integer | Integer flag, `1` when the entry is live. The public feed only returns published entries, so this is `1` on every row a consumer sees. |
| `data[].createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data[].updatedAt` | string | When it last changed, ISO 8601. |
| `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": "043-webhooks-that-actually-fire",
      "title": "Ammunition ballistics endpoints",
      "body": "string",
      "category": "feature",
      "publishedAt": "2026-08-01T00:00:00.000Z",
      "published": 1,
      "createdAt": "2026-09-10T03:57:09.092Z",
      "updatedAt": "2026-09-10T08:31:20.586Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 42,
    "totalPages": 3
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- No API key required. Only published entries are returned.
- Categories: `feature`, `improvement`, `bugfix`, `data`, `breaking`.

### Example

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

## Get Changelog Entry

`GET /v1/changelog/{id}`

Auth: Optional API key  
Tier: Explorer+

Returns a single changelog entry.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Changelog entry id |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | ChangelogEntry | The response payload |
| `data.id` | string | Slug of the entry, taken from the file it was authored in. |
| `data.title` | string | Entry headline. |
| `data.body` | string | Entry body, in Markdown. |
| `data.category` | string \| null | What kind of change: `feature`, `improvement`, `bugfix`, `data`, `breaking`. |
| `data.publishedAt` | string | When it was published, ISO 8601. |
| `data.published` | integer | Integer flag, `1` when the entry is live. The public feed only returns published entries, so this is `1` on every row a consumer sees. |
| `data.createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data.updatedAt` | string | When it last changed, ISO 8601. |

```json
{
  "success": true,
  "data": {
    "id": "043-webhooks-that-actually-fire",
    "title": "Ammunition ballistics endpoints",
    "body": "string",
    "category": "feature",
    "publishedAt": "2026-08-01T00:00:00.000Z",
    "published": 1,
    "createdAt": "2026-09-10T03:57:09.092Z",
    "updatedAt": "2026-09-10T08:31:20.586Z"
  }
}
```

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

### Example

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

## List site updates

`GET /v1/updates`

Auth: Optional API key  
Tier: Explorer+

Returns published posts about the service, pinned first and then newest. This is the editorial feed behind `gunspec.io/updates`: what is happening with the catalogue, the platform or the company, and why. `/v1/changelog` is the separate, narrower record of what shipped.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Updates per page |
| `kind` | string | no | Filter by what the post is about |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | SiteUpdate[] | A dated post about the service: what is happening with the catalogue, the platform or the company, and why. Distinct from `/v1/changelog`, which records what shipped, and from a site notice, which is one line of chrome. |
| `data[].id` | string | The slug, and the URL: `gunspec.io/updates/{id}`. |
| `data[].title` | string |  |
| `data[].summary` | string |  |
| `data[].kind` | UpdateKind | What a site update is about: the catalogue, the platform, a policy, the company, or an incident. |
| `data[].body` | UpdateBlock[] |  |
| `data[].body[].type` | UpdateBlockType | The kind of block in a site update body. Each maps to a renderer on the website. |
| `data[].body[].text` | string | The words, on a `paragraph`, `heading` or `quote`. |
| `data[].body[].items` | string[] | The entries of a `list`. |
| `data[].body[].ordered` | boolean | Whether a `list` is numbered. |
| `data[].body[].attribution` | string | Who a `quote` is from. |
| `data[].body[].code` | string | The source of a `code` block. |
| `data[].body[].language` | string | How to highlight a `code` block. |
| `data[].body[].figure` | UpdateFigure | The figure beside an update. Either a picture, or a character-field pattern the website draws itself: most posts have no photograph, and a named drawing is the honest alternative to a stock image. Read `kind` to tell them apart. |
| `data[].body[].caption` | string | The line under a `figure`. |
| `data[].figure` | UpdateFigure | The figure beside an update. Either a picture, or a character-field pattern the website draws itself: most posts have no photograph, and a named drawing is the honest alternative to a stock image. Read `kind` to tell them apart. |
| `data[].figure.kind` | string | What the asset is: `silhouette`, `render`, `photo`, `schematic`, `model`. `/v1/firearms/{id}/media/{selector}` addresses an asset by this. |
| `data[].figure.url` | string | Where the picture is. Present when `kind` is `image`. |
| `data[].figure.alt` | string | What the picture shows. Present when `kind` is `image`. |
| `data[].figure.pattern` | UpdateGlyphPattern | A character-field pattern an update may name in place of a photograph. The website draws it. |
| `data[].figure.seed` | string | Seeds the drawing. The same seed always draws the same field, so a post keeps its figure between loads. |
| `data[].figure.words` | string[] | What the `words` pattern spells. Ignored by the others. |
| `data[].publishedAt` | string \| null | The day the post is dated, as `YYYY-MM-DD`. A later edit does not re-date it. |
| `data[].pinned` | integer | Held at the top of the list regardless of date. |
| `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": "firearm-intake-paused",
      "title": "Firearm intake is paused while quality checks land",
      "summary": "No new firearms this month. Here is what we are building instead.",
      "kind": "catalog",
      "body": [
        {
          "type": "paragraph",
          "text": "We have paused firearm intake while the quality checks land.",
          "items": [
            "string"
          ],
          "ordered": false,
          "attribution": "string",
          "code": "string",
          "language": "bash",
          "figure": {
            "kind": "glyph",
            "url": "https://assets.gunspec.io/updates/intake.webp",
            "alt": "A bench of rifles awaiting measurement.",
            "pattern": "impact",
            "seed": "intake-pause",
            "words": [
              "measured",
              "checked"
            ]
          },
          "caption": "string"
        }
      ],
      "figure": {
        "kind": "glyph",
        "url": "https://assets.gunspec.io/updates/intake.webp",
        "alt": "A bench of rifles awaiting measurement.",
        "pattern": "impact",
        "seed": "intake-pause",
        "words": [
          "measured",
          "checked"
        ]
      },
      "publishedAt": "2026-09-21",
      "pinned": 0
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 42,
    "totalPages": 3
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

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

## Get a site update

`GET /v1/updates/{id}`

Auth: Optional API key  
Tier: Explorer+

Returns one published post, with its body blocks.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | The update slug |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.update` | SiteUpdate | A dated post about the service: what is happening with the catalogue, the platform or the company, and why. Distinct from `/v1/changelog`, which records what shipped, and from a site notice, which is one line of chrome. |
| `data.update.id` | string | The slug, and the URL: `gunspec.io/updates/{id}`. |
| `data.update.title` | string |  |
| `data.update.summary` | string |  |
| `data.update.kind` | UpdateKind | What a site update is about: the catalogue, the platform, a policy, the company, or an incident. |
| `data.update.body` | UpdateBlock[] |  |
| `data.update.figure` | UpdateFigure | The figure beside an update. Either a picture, or a character-field pattern the website draws itself: most posts have no photograph, and a named drawing is the honest alternative to a stock image. Read `kind` to tell them apart. |
| `data.update.publishedAt` | string \| null | The day the post is dated, as `YYYY-MM-DD`. A later edit does not re-date it. |
| `data.update.pinned` | integer | Held at the top of the list regardless of date. |

```json
{
  "success": true,
  "data": {
    "update": {
      "id": "firearm-intake-paused",
      "title": "Firearm intake is paused while quality checks land",
      "summary": "No new firearms this month. Here is what we are building instead.",
      "kind": "catalog",
      "body": [
        {
          "type": "paragraph",
          "text": "We have paused firearm intake while the quality checks land.",
          "items": [
            "string"
          ],
          "ordered": false,
          "attribution": "string",
          "code": "string",
          "language": "bash",
          "figure": {
            "kind": "glyph",
            "url": "https://assets.gunspec.io/updates/intake.webp",
            "alt": "A bench of rifles awaiting measurement.",
            "pattern": "impact",
            "seed": "intake-pause",
            "words": [
              "measured",
              "checked"
            ]
          },
          "caption": "string"
        }
      ],
      "figure": {
        "kind": "glyph",
        "url": "https://assets.gunspec.io/updates/intake.webp",
        "alt": "A bench of rifles awaiting measurement.",
        "pattern": "impact",
        "seed": "intake-pause",
        "words": [
          "measured",
          "checked"
        ]
      },
      "publishedAt": "2026-09-21",
      "pinned": 0
    }
  }
}
```

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

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/updates/firearm-intake-paused' \
  --header 'X-API-Key: your_key'
```

## List active site notices

`GET /v1/notices`

Auth: Optional API key  
Tier: Explorer+

Returns the operator-authored banners the website is currently showing, best first. Only published notices inside their display window appear, so an empty array is the normal state.

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.notices` | SiteNotice[] | Published notices inside their display window, best first: highest priority, then newest. |
| `data.notices[].id` | string | Row id of the notice. The website keys its dismissal on this, so a new notice is seen even by a reader who dismissed the last one. |
| `data.notices[].label` | string | Short label shown before the message. |
| `data.notices[].highlight` | string | The sentence shown in the banner. |
| `data.notices[].detail` | string | A second line, shown under the highlight where one is set. |
| `data.notices[].variant` | NoticeVariant | How prominently a site notice is shown. |
| `data.notices[].ctaLabel` | string \| null | Text of the call to action, or null when the banner has none. |
| `data.notices[].ctaUrl` | string \| null | Where the call to action goes. |
| `data.notices[].priority` | integer | Higher sorts first. Ties break on newest. |
| `data.notices[].startsAt` | string \| null | When the notice becomes visible. Null means immediately. |
| `data.notices[].endsAt` | string \| null | When it stops being shown. This is why notices are rows rather than copy: an end date is the only version of "take this down later" that does not rely on somebody remembering. |

```json
{
  "success": true,
  "data": {
    "notices": [
      {
        "id": "silhouettes-2026-05",
        "label": "Notice",
        "highlight": "Scheduled maintenance on Sunday.",
        "detail": "The API stays up; the website may be briefly unavailable.",
        "variant": "info",
        "ctaLabel": "Read the changelog",
        "ctaUrl": "/changelog",
        "priority": 0,
        "startsAt": "2026-05-01T00:00:00.000Z",
        "endsAt": "2026-09-30T00:00:00.000Z"
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

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

## Latest SDK example verification run

`GET /v1/sdk/verification`

Auth: Optional API key  
Tier: Explorer+

Every example on the SDK reference is executed against this API on a schedule and after each SDK release: the published package, a fresh project, the snippet exactly as printed. This returns the most recent run for each language, with the outcome of each example. Write examples are never run against production, since they would create a real ticket, webhook endpoint or seller offer, and are reported as `skipped` with that reason, so the counts never imply coverage the run does not have.

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.runs` | SdkVerificationRun[] | The latest run per language, ordered by language. |
| `data.runs[].surface` | string | Which promise the run is about: `sdk` for the published packages, `api` for the cURL commands the reference prints. |
| `data.runs[].language` | string | The sample's own language. On an SDK run, `typescript` or `python` for the published package. On an API run, the tab of the reference it came from: `curl`, `javascript` (the TypeScript tab's generated `fetch`), `python` (its generated `requests`), or `unity`, `unreal` and `godot`, which are checked rather than executed. |
| `data.runs[].checkKind` | string | How the run arrived at its results. `executed` ran the sample against this API. `static` could not, and checked the sample against the operation it documents instead: the Unity, Unreal and GDScript samples are fragments meant for a class, an actor or a scene, so running one would mean wrapping it in code the reference does not print. |
| `data.runs[].sdkVersion` | string | What was exercised: the release installed from the registry on an SDK run, and the spec version the samples were generated from on an API run. The release rather than the version in our tree, because the badge vouches for what a reader gets today. |
| `data.runs[].apiHost` | string | The API the run was executed against. Production, so the badge vouches for the live service rather than a preview. |
| `data.runs[].ranAt` | string | When the run finished, ISO 8601. |
| `data.runs[].commitSha` | string \| null | The commit of this repository the run was started from, or null where CI did not report one. |
| `data.runs[].runUrl` | string \| null | The CI run, so the claim can be checked rather than taken. |
| `data.runs[].passed` | integer | Examples that ran and returned what the page prints. |
| `data.runs[].failed` | integer | Examples that ran and did not. A non-zero count is what takes the badge down. |
| `data.runs[].skipped` | integer | Write examples, which are never run against production: each would create a real ticket, webhook endpoint or seller offer. |
| `data.runs[].unsupported` | integer | Examples this SDK has no method for. The reference shows no sample for them either. |

```json
{
  "success": true,
  "data": {
    "runs": [
      {
        "surface": "api",
        "language": "typescript",
        "checkKind": "executed",
        "sdkVersion": "0.2.3",
        "apiHost": "https://api.gunspec.io",
        "ranAt": "2026-09-13T03:15:00.000Z",
        "commitSha": "9f1c2ab",
        "runUrl": "https://github.com/buun-group/gunspec/actions/runs/123456",
        "passed": 118,
        "failed": 0,
        "skipped": 13,
        "unsupported": 11,
        "examples": [
          {
            "exampleId": "sdk-firearms-list",
            "resource": "firearms",
            "method": "list",
            "endpoint": "GET /v1/firearms",
            "status": "passed",
            "durationMs": 412,
            "reason": null,
            "hasSample": true,
            "sample": {
              "id": "glock-g17",
              "name": "Glock 17"
            }
          }
        ]
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

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

## Latest example verification runs

`GET /v1/examples/verification`

Auth: Optional API key  
Tier: Explorer+

The receipts behind the "verified" badges in the documentation, for every surface. The SDK surface installs the published package into a fresh project and runs each sample exactly as printed; the API surface runs the cURL command exactly as the reference prints it, with the key substituted at run time; the MCP surface calls every tool on the hosted MCP server with its documented example arguments. This returns the most recent run for each surface and language, with the outcome of every example. Examples that would write are never run against production, since each would create a real ticket, webhook endpoint or seller offer, and are reported as `skipped` with that reason, so the counts never imply coverage the run does not have.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `surface` | string | no | Narrow to one surface: `sdk` for the SDK references, `api` for the reference samples, `mcp` for the hosted MCP server's tools. Omitted returns every surface. |
| `include` | string | no | `samples` adds `sample` to each example: what it answered when the run executed it, trimmed for display. Only the MCP surface stores answers today; every example says whether it has one in `hasSample` either way. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.runs` | SdkVerificationRun[] | The latest run per surface and language, ordered by surface then language. |
| `data.runs[].surface` | string | Which promise the run is about: `sdk` for the published packages, `api` for the cURL commands the reference prints. |
| `data.runs[].language` | string | The sample's own language. On an SDK run, `typescript` or `python` for the published package. On an API run, the tab of the reference it came from: `curl`, `javascript` (the TypeScript tab's generated `fetch`), `python` (its generated `requests`), or `unity`, `unreal` and `godot`, which are checked rather than executed. |
| `data.runs[].checkKind` | string | How the run arrived at its results. `executed` ran the sample against this API. `static` could not, and checked the sample against the operation it documents instead: the Unity, Unreal and GDScript samples are fragments meant for a class, an actor or a scene, so running one would mean wrapping it in code the reference does not print. |
| `data.runs[].sdkVersion` | string | What was exercised: the release installed from the registry on an SDK run, and the spec version the samples were generated from on an API run. The release rather than the version in our tree, because the badge vouches for what a reader gets today. |
| `data.runs[].apiHost` | string | The API the run was executed against. Production, so the badge vouches for the live service rather than a preview. |
| `data.runs[].ranAt` | string | When the run finished, ISO 8601. |
| `data.runs[].commitSha` | string \| null | The commit of this repository the run was started from, or null where CI did not report one. |
| `data.runs[].runUrl` | string \| null | The CI run, so the claim can be checked rather than taken. |
| `data.runs[].passed` | integer | Examples that ran and returned what the page prints. |
| `data.runs[].failed` | integer | Examples that ran and did not. A non-zero count is what takes the badge down. |
| `data.runs[].skipped` | integer | Write examples, which are never run against production: each would create a real ticket, webhook endpoint or seller offer. |
| `data.runs[].unsupported` | integer | Examples this SDK has no method for. The reference shows no sample for them either. |

```json
{
  "success": true,
  "data": {
    "runs": [
      {
        "surface": "api",
        "language": "typescript",
        "checkKind": "executed",
        "sdkVersion": "0.2.3",
        "apiHost": "https://api.gunspec.io",
        "ranAt": "2026-09-13T03:15:00.000Z",
        "commitSha": "9f1c2ab",
        "runUrl": "https://github.com/buun-group/gunspec/actions/runs/123456",
        "passed": 118,
        "failed": 0,
        "skipped": 13,
        "unsupported": 11,
        "examples": [
          {
            "exampleId": "sdk-firearms-list",
            "resource": "firearms",
            "method": "list",
            "endpoint": "GET /v1/firearms",
            "status": "passed",
            "durationMs": 412,
            "reason": null,
            "hasSample": true,
            "sample": {
              "id": "glock-g17",
              "name": "Glock 17"
            }
          }
        ]
      }
    ]
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

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

## Contract status badge

`GET /v1/contract/badge.svg`

Auth: Optional API key  
Tier: Explorer+

The latest run's verdict as an SVG badge, for a README or a directory listing: green with the number of checks that passed, red with the number that failed, or grey when nothing has been run. Drawn by this API from the same record `GET /v1/contract` serves, so the badge and the page cannot disagree. Cached for five minutes.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `surface` | string | no | Which product to report: `api` for this REST API, `mcp` for the hosted MCP server, `fitment` for the attachment compatibility suite. |
| `env` | string | no | Which environment's run to report. Production unless asked otherwise. |
| `label` | string | no | The wording on the grey half. Defaults to `api contract` or `mcp health`. Bounded because it is drawn into an image. |
| `logo` | string | no | Set `0` to draw the badge without the mark, for a row of badges that carry their own. |

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

```json
"string"
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

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

## Latest contract check run

`GET /v1/contract`

Auth: Optional API key  
Tier: Explorer+

Every documented operation is called against this API on a schedule and held against this document: the status, the media type, the schema, the envelope, the fields promised against the fields sent, the cache contract, and the plan gates. This returns the most recent run, with the verdict for each operation, the checks that failed, and what happened when each gate was exercised with a key above it, at it and below it.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `env` | string | no | Which environment to report. Production unless asked otherwise; a staging run is never mixed into it. |
| `surface` | string | no | Which product the run measured: `api` for this REST API, `mcp` for the hosted MCP server, `fitment` for the attachment compatibility suite. Defaults to `api`. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.run` | ApiContractRun \| null | The latest run, or null when nothing has been checked on this environment yet. |

```json
{
  "success": true,
  "data": {
    "run": {
      "surface": "api",
      "env": "production",
      "specVersion": "1.1.0",
      "commitSha": "9f1c2ab",
      "runUrl": "https://github.com/buun-group/gunspec/actions/runs/123456",
      "ranAt": "2026-09-13T03:15:00.000Z",
      "durationMs": 28837,
      "operations": 128,
      "passed": 110,
      "failed": 2,
      "skipped": 16,
      "tiers": [
        "studio"
      ],
      "results": [
        {
          "key": "get /v1/firearms/{id}",
          "method": "get",
          "path": "/v1/firearms/{id}",
          "tag": "Firearms",
          "tier": "explorer",
          "status": 200,
          "ms": 41,
          "verdict": "pass",
          "checks": {
            "passed": 12,
            "failed": 0,
            "skipped": 2
          },
          "describes": null,
          "failures": [
            {
              "check": "schema",
              "detail": "/data/[]/id must be integer"
            }
          ],
          "access": [
            {
              "check": "tier-refuses",
              "tier": "builder",
              "verdict": "pass",
              "detail": "builder key refused 403 PLAN_REQUIRED, as `x-gunspec-tier: studio` requires"
            }
          ]
        }
      ],
      "systemChecks": [
        {
          "id": "json-not-stringified",
          "describes": "No response carries JSON inside a string.",
          "status": "pass",
          "found": 0,
          "checked": 127,
          "sample": [
            "get /v1/firearms/by-feature"
          ]
        }
      ]
    }
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Example

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

## Look up a documented operation

`GET /v1/docs/operations`

Auth: API key required  
Tier: Explorer+

Returns what the API reference says about calling an operation: its parameters and what each accepts, the request body, the plan it needs, whether it answers `304 Not Modified`, every failure it can return with the `error.reason` values a client branches on, and the languages a sample is printed in. `path` may be a documented template (`/v1/firearms/{id}`), a concrete path (`/v1/firearms/glock-g17`) or a full URL. A path that names no operation returns the operations under it, so `/v1/vendor` lists what a seller integration can call. Generated from the reference itself, so it cannot describe an operation differently from the page.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | string | yes | The operation's path: a documented template, a concrete path or a full URL. A path that names no operation is matched as a prefix. |
| `method` | string | no | Narrow to one HTTP method. Omitted returns every method the path answers. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.matched` | string | `exact` when the path named an operation, `prefix` when it did not and these are the operations under it (at most 25). |
| `data.specVersion` | string | The version of this document the reference was rendered from. |
| `data.operations` | object[] | The matching operations. |
| `data.operations[].method` | string | HTTP method. |
| `data.operations[].path` | string | The documented path template. |
| `data.operations[].title` | string | The operation's title in the reference. |
| `data.operations[].description` | string | What the operation does, as the reference states it. |
| `data.operations[].tier` | string | The lowest plan that may call it: `public`, `account` (a key attached to a user account), `explorer`, `builder`, `studio` or `enterprise`. |
| `data.operations[].auth` | string | Whether a key is `required`, `optional` or not used (`none`). |
| `data.operations[].cacheable` | boolean | Whether it answers `304 Not Modified` to an `If-None-Match` carrying the ETag it sent. |
| `data.operations[].docsUrl` | string | The operation's page in the reference. |
| `data.operations[].parameters` | object[] | Path, query and header parameters, as the reference lists them. |
| `data.operations[].requestBody` | object \| null | The request body, or null for an operation that takes none. |
| `data.operations[].success` | object | What success looks like. |
| `data.operations[].errors` | object[] | Every failure the operation documents. |

```json
{
  "success": true,
  "data": {
    "matched": "exact",
    "specVersion": "1.0.0",
    "operations": [
      {
        "method": "GET",
        "path": "/v1/firearms/{id}",
        "title": "Get Firearm",
        "description": "Returns the full specification of one firearm.",
        "tier": "builder",
        "auth": "required",
        "cacheable": true,
        "docsUrl": "https://docs.gunspec.io/en/api/firearms/get",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "type": "string",
            "required": true,
            "description": "Firearm slug",
            "accepts": [
              "string"
            ],
            "default": "20",
            "minimum": 1,
            "maximum": 100
          }
        ],
        "requestBody": {
          "contentType": "application/json",
          "example": "{\"name\": \"Glock 19\"}"
        },
        "success": {
          "status": 200,
          "contentType": "application/json"
        },
        "errors": [
          {
            "status": 404,
            "message": "Not found",
            "code": "NOT_FOUND",
            "reasons": [
              "RESOURCE_NOT_FOUND"
            ]
          }
        ],
        "notes": [
          "string"
        ],
        "sampleLanguages": [
          "python"
        ],
        "sdk": [
          {
            "typescript": "client.firearms.get(id)",
            "python": "client.firearms.get(id: str)"
          }
        ]
      }
    ]
  }
}
```

### 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/docs/operations?path=%2Fv1%2Ffirearms%2F%7Bid%7D' \
  --header 'X-API-Key: your_key'
```

## Get a documented code sample

`GET /v1/docs/samples`

Auth: API key required  
Tier: Explorer+

Returns the sample the reference prints for an operation in one language, exactly as printed, with the result of the last run that checked it. The cURL, TypeScript `fetch` and Python `requests` samples and the TypeScript and Python SDK samples are executed against production on a schedule; the Unity, Unreal and Godot samples are checked, not executed. A sample is never composed for the request: a language the reference does not print for that operation is a 404 naming the languages it does.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `path` | string | yes | The operation's path: a documented template, a concrete path or a full URL. |
| `method` | string | no | The HTTP method. Needed only when the path answers more than one. |
| `language` | string | yes | `curl`, `javascript` (TypeScript with `fetch`), `python` (`requests`), `unity`, `unreal`, `godot`, or `sdk-typescript` / `sdk-python` for the SDKs. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.method` | string | HTTP method of the operation. |
| `data.path` | string | Its documented path template. |
| `data.tier` | string | The lowest plan that may call it. |
| `data.docsUrl` | string | The operation's page in the reference. |
| `data.language` | string | The language asked for. |
| `data.keyNote` | string \| null | How to fill in the key placeholder, or null for an operation that takes no key. |
| `data.samples` | object[] | One sample, or one per SDK method for an operation more than one method calls. |
| `data.samples[].label` | string | What the sample is: the tab name, or the SDK call. |
| `data.samples[].code` | string | The sample, exactly as the reference prints it. |
| `data.samples[].verification` | object \| null | The last run that checked this sample, or null when none has. |
| `data.samples[].note` | string \| null | Anything to know before using it, such as why it is not executed, or null. |

```json
{
  "success": true,
  "data": {
    "method": "GET",
    "path": "/v1/firearms",
    "tier": "explorer",
    "docsUrl": "https://docs.gunspec.io/en/api/firearms/list",
    "language": "python",
    "keyNote": "Replace your_key with a GunSpec API key, sent as the X-API-Key header.",
    "samples": [
      {
        "label": "Python (requests)",
        "code": "import requests",
        "verification": {
          "status": "passed",
          "checkKind": "executed",
          "ranAt": "2026-09-17T08:50:16.655Z"
        },
        "note": null
      }
    ]
  }
}
```

### Errors

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

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/docs/samples?path=%2Fv1%2Ffirearms&language=python' \
  --header 'X-API-Key: your_key'
```

## Get plan limits

`GET /v1/docs/limits`

Auth: API key required  
Tier: Explorer+

Returns what each plan allows, from the configuration the API enforces: requests per minute, per day and per month, MCP calls per day, how deep a list may be paged, and whether list responses carry a total count. Also how a burst of paging is limited, how long to wait after each refusal, and the headers a key may be sent in. A limit a plan does not have is null.

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.plans` | object[] | Every plan, cheapest first. |
| `data.plans[].tier` | string | Plan id. |
| `data.plans[].name` | string | Plan name. |
| `data.plans[].priceUsdCentsPerMonth` | integer | Monthly price in US cents. |
| `data.plans[].requestsPerMinute` | integer | Requests allowed per minute, per key. |
| `data.plans[].requestsPerDay` | integer | Requests allowed per day, per key. |
| `data.plans[].requestsPerMonth` | integer | Requests included per month. |
| `data.plans[].mcpCallsPerDay` | integer | Calls through the hosted MCP server allowed per day, inside the daily request allowance. |
| `data.plans[].maxPage` | integer \| null | The deepest page a list may be read to, or null for no limit. |
| `data.plans[].totalCountInLists` | boolean | Whether paginated responses include the total count. |
| `data.withoutKey` | object | Limits on a caller that sends no key. |
| `data.withoutKey.requestsPerDay` | integer | Requests allowed per day to the endpoints that need no key. |
| `data.withoutKey.maxPage` | integer \| null | The deepest page a list may be read to without a key. |
| `data.paginationBurst` | object | How fast a list may be paged through before `PAGINATION_BURST` is returned. |
| `data.paginationBurst.pages` | integer | Pages that may be read in quick succession. |
| `data.paginationBurst.windowSeconds` | integer | The window those pages are counted over. |
| `data.retryAfterSeconds` | object | How long to wait after a 429. |
| `data.retryAfterSeconds.rateLimited` | integer | The `Retry-After` sent with a per-minute refusal. |
| `data.retryAfterSeconds.dailyCapReached` | integer | The `Retry-After` sent when the daily allowance is spent. |
| `data.keyHeaders` | string[] | The headers a key may be sent in. |
| `data.docsUrl` | string | The rate limits page in the docs. |

```json
{
  "success": true,
  "data": {
    "plans": [
      {
        "tier": "builder",
        "name": "Builder",
        "priceUsdCentsPerMonth": 2900,
        "requestsPerMinute": 60,
        "requestsPerDay": 2000,
        "requestsPerMonth": 25000,
        "mcpCallsPerDay": 500,
        "maxPage": 25,
        "totalCountInLists": true
      }
    ],
    "withoutKey": {
      "requestsPerDay": 50,
      "maxPage": 5
    },
    "paginationBurst": {
      "pages": 10,
      "windowSeconds": 90
    },
    "retryAfterSeconds": {
      "rateLimited": 60,
      "dailyCapReached": 3600
    },
    "keyHeaders": [
      "X-API-Key: <key>"
    ],
    "docsUrl": "https://docs.gunspec.io/en/rate-limits"
  }
}
```

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

## List the documentation guides

`GET /v1/docs/guides`

Auth: API key required  
Tier: Explorer+

Returns every guide the documentation renders, with the headings each is divided into. Use it to see what is documented before searching, or to find the id and anchor of a section to read. The text is the page's own, so a guide cannot say something here that it does not say to a reader.

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.total` | integer | How many guides there are. |
| `data.guides` | object[] | Every guide, in the order the documentation lists them. |
| `data.guides[].id` | string | The guide's id, passed to the read endpoint. |
| `data.guides[].title` | string | The page's title. |
| `data.guides[].url` | string | The page a reader would open. |
| `data.guides[].summary` | string | The opening prose: what the page is about. |
| `data.guides[].sizeBytes` | integer | The whole page in Markdown, so a caller can tell what reading it costs. |
| `data.guides[].sections` | object[] | Every heading on the page, in order. |

```json
{
  "success": true,
  "data": {
    "total": 32,
    "guides": [
      {
        "id": "caching",
        "title": "Caching and refreshing",
        "url": "https://docs.gunspec.io/en/caching",
        "summary": "You are encouraged to keep a local copy of the catalog.",
        "sizeBytes": 10038,
        "sections": [
          {
            "anchor": "caching-policy",
            "title": "How long you may keep it",
            "trail": "How long you may keep it"
          }
        ]
      }
    ]
  }
}
```

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

## Search the documentation guides

`GET /v1/docs/guides/search`

Auth: API key required  
Tier: Explorer+

Returns the sections of the guides that answer a question, best first, each with a snippet and a link to the heading it came from. This is the entry point: search, then read the one section you need rather than a whole page. A question the guides do not cover returns no results rather than the nearest paragraph.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string | yes | The question, in the words it would be asked in. |
| `limit` | integer | no | How many sections to return. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.query` | string | The question as it was asked. |
| `data.total` | integer | How many sections matched, of which `results` carries the best. |
| `data.results` | object[] | The matching sections, best first. |
| `data.results[].guideId` | string | The guide the section is on. |
| `data.results[].guideTitle` | string | That guide's title. |
| `data.results[].anchor` | string | The section's anchor. |
| `data.results[].title` | string | The heading. |
| `data.results[].trail` | string | The heading with the ones above it. |
| `data.results[].url` | string | The page, opened at that heading. |
| `data.results[].snippet` | string | The opening of the section, so a caller can judge it without reading it. |

```json
{
  "success": true,
  "data": {
    "query": "how do I handle a 429",
    "total": 34,
    "results": [
      {
        "guideId": "rate-limits",
        "guideTitle": "Rate Limits & Plans",
        "anchor": "rate-limits-retry",
        "title": "What to do when you are limited",
        "trail": "What to do when you are limited",
        "url": "https://docs.gunspec.io/en/rate-limits#rate-limits-retry",
        "snippet": "A 429 carries Retry-After in seconds."
      }
    ]
  }
}
```

### 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/docs/guides/search?q=how%20do%20I%20handle%20a%20429' \
  --header 'X-API-Key: your_key'
```

## Read a documentation guide

`GET /v1/docs/guides/{id}`

Auth: API key required  
Tier: Explorer+

Returns one guide as Markdown. With `anchor`, only that heading, which is what an answer usually needs; without it, the whole page, which can run to tens of kilobytes. Find an id and its anchors with the list or search endpoint.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | The guide's id, as the list endpoint gives it. |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `anchor` | string | no | Return only the section this heading anchor names. Omitted, the whole page comes back. |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.id` | string | The guide's id. |
| `data.title` | string | The page's title. |
| `data.url` | string | The page, opened at the section when one was named. |
| `data.summary` | string | The opening prose. Sent with a whole page only. |
| `data.markdown` | string | The whole page as Markdown. Sent when no anchor was given. |
| `data.sections` | object[] | The page's headings, or the named section - one anchor can hold several headings where a page deep-links only the outer one. |
| `data.sections[].anchor` | string | The heading's anchor. |
| `data.sections[].title` | string | The heading. |
| `data.sections[].trail` | string | The heading with the ones above it. |
| `data.sections[].markdown` | string | The section's prose. Sent when an anchor was given. |

```json
{
  "success": true,
  "data": {
    "id": "caching",
    "title": "Caching and refreshing",
    "url": "https://docs.gunspec.io/en/caching#caching-policy",
    "summary": "You are encouraged to keep a local copy of the catalog.",
    "markdown": "## How long you may keep it\n\nEvery lifetime below…",
    "sections": [
      {
        "anchor": "caching-policy",
        "title": "How long you may keep it",
        "trail": "How long you may keep it",
        "markdown": "Every lifetime below is the value the API sends…"
      }
    ]
  }
}
```

### 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/docs/guides/caching' \
  --header 'X-API-Key: your_key'
```

## List Blog Posts

`GET /v1/blog`

Auth: Optional API key  
Tier: Explorer+

Returns published blog posts, newest first.

### 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 | Filter by post category |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | BlogPost[] | A published post. Both endpoints serve the stored row; `body` is markdown. |
| `data[].id` | string | Opaque post id. A string, not a number: posts are keyed like every other authored record here. |
| `data[].slug` | string | URL-safe slug, and what `/v1/blog/{slug}` addresses. |
| `data[].title` | string | Post headline. |
| `data[].summary` | string \| null | One-line description, for a card or a feed. Null where the author wrote none. |
| `data[].body` | string \| null | The post itself, as markdown. |
| `data[].heroImage` | string \| null | Path of the hero image on the assets CDN, or null. Prefix it with the assets host to fetch. |
| `data[].category` | string \| null | Editorial category, e.g. `guides`. Null where the post has none. |
| `data[].status` | string | Always `published` on these endpoints: a draft is not served publicly. |
| `data[].postedAt` | string \| null | When the post was published, which is what the list orders by. Null on a post never given a date. |
| `data[].createdAt` | string | When the post was first written, `YYYY-MM-DD HH:MM:SS` in UTC. Not the publication date: `postedAt` is. |
| `data[].updatedAt` | string | When the post was last edited, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `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": "post_01hbar",
      "slug": "complete-guide-firearm-calibers-for-developers",
      "title": "What is a firearms specification API",
      "summary": "What the catalog holds, and how to call it.",
      "body": null,
      "heroImage": "guides/what-is-a-firearms-api.webp",
      "category": "guides",
      "status": "published",
      "postedAt": "2026-08-01T00:00:00.000Z",
      "createdAt": "2026-08-01 09:12:44",
      "updatedAt": "2026-08-02 10:01:07"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 42,
    "totalPages": 3
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- No API key required. Only `published` posts are returned.
- Posts are ordered by `postedAt` descending (most recent first).

### Example

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

## Get Blog Post

`GET /v1/blog/{slug}`

Auth: Optional API key  
Tier: Explorer+

Returns a single published blog post, including its full body.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | yes | Blog post slug |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.post` | BlogPost | A published post. Both endpoints serve the stored row; `body` is markdown. |
| `data.post.id` | string | Opaque post id. A string, not a number: posts are keyed like every other authored record here. |
| `data.post.slug` | string | URL-safe slug, and what `/v1/blog/{slug}` addresses. |
| `data.post.title` | string | Post headline. |
| `data.post.summary` | string \| null | One-line description, for a card or a feed. Null where the author wrote none. |
| `data.post.body` | string \| null | The post itself, as markdown. |
| `data.post.heroImage` | string \| null | Path of the hero image on the assets CDN, or null. Prefix it with the assets host to fetch. |
| `data.post.category` | string \| null | Editorial category, e.g. `guides`. Null where the post has none. |
| `data.post.status` | string | Always `published` on these endpoints: a draft is not served publicly. |
| `data.post.postedAt` | string \| null | When the post was published, which is what the list orders by. Null on a post never given a date. |
| `data.post.createdAt` | string | When the post was first written, `YYYY-MM-DD HH:MM:SS` in UTC. Not the publication date: `postedAt` is. |
| `data.post.updatedAt` | string | When the post was last edited, `YYYY-MM-DD HH:MM:SS` in UTC. |

```json
{
  "success": true,
  "data": {
    "post": {
      "id": "post_01hbar",
      "slug": "complete-guide-firearm-calibers-for-developers",
      "title": "What is a firearms specification API",
      "summary": "What the catalog holds, and how to call it.",
      "body": null,
      "heroImage": "guides/what-is-a-firearms-api.webp",
      "category": "guides",
      "status": "published",
      "postedAt": "2026-08-01T00:00:00.000Z",
      "createdAt": "2026-08-01 09:12:44",
      "updatedAt": "2026-08-02 10:01:07"
    }
  }
}
```

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

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/blog/complete-guide-firearm-calibers-for-developers' \
  --header 'X-API-Key: your_key'
```
