GunSpec
Versioning

Versioning

What the version in the path promises, what we change without touching it, how a breaking change ships, and the one narrow case where the current version is corrected in place instead. The prefix, the version and the policy figures are read from the API's own configuration.

  • /v1Path prefix
  • 1.1.0Spec version
  • 133Operations under it
  • 0Deprecated operations

Every endpoint lives under /v1. That prefix is the contract: while it answers, a request that worked keeps working. The OpenAPI document's info.version (1.1.0) says which release of that contract is live, and its major number is the prefix.

Two places a version appears
bash
# The prefix is the contractcurl -H "X-API-Key: your_api_key" https://api.gunspec.io/v1/firearms/glock-17-gen5 # The document says which release of that contract is livecurl https://api.gunspec.io/openapi.json | jq .info.version# "1.1.0"
1Major: the prefix
Moves only with a breaking change, and then the path moves with it: /v1 becomes /v2. Nothing under the old prefix changes shape on that day.
1Minor: additive
Moves when something is added: an endpoint, a field, a parameter, an enum value, an event. A client that ignores what it does not know sees no difference.
0Patch: fixes
Moves for a correction that changes no shape: a wrong description, a figure that was off, a validation that was looser than documented.

2 paths sit outside the prefix because they describe the service rather than the data, and are stable by design: /health, /ready. The version field on a catalog record is unrelated: it is a fingerprint of that record's data for caching and mirroring, described under Data revisions and in the field reference, and it changes when the record does, not when the API does.

Additive changes land under /v1 as they are ready, with a minor bump to info.version and a changelog entry. A client written against the rules below never notices them.

  • New endpoints, and new optional query parameters on existing ones. An unknown parameter is ignored, never refused.
  • New fields on a response. They arrive alongside the ones you read; nothing you read moves or changes type.
  • New values in a closed vocabulary. A record can carry a status, an action type or an event name that did not exist when your code was written.
  • New error.reason values under an existing error.code. The code is the family your code branches on; the reason is the specific situation, and the list grows.
  • More data. Records gain fields that were null, counts go up, lists get longer. A figure that was null becoming a value is not a change of shape.

Vocabularies that grow

  • firearm.status6 today

    A record's production status. Match the values you handle and treat the rest as unknown, never as an error.

    • in_production
    • discontinued
    • out_of_production
    • all 6
  • firearm.actionType46 today

    The mechanism a firearm uses. The longest of the four, and the one most often switched over exhaustively.

    • short_recoil
    • bolt_action
    • blowback
    • all 46
  • X-Webhook-Event18 today

    What a delivery is about. A subscriber to a wildcard receives new event names as they ship.

    • firearm.created
    • firearm.updated
    • firearm.deleted
    • all 18
  • error.reason39 today

    Why a request was refused, one level under the status. Branch on the code first, then on the reasons you can act on.

    • INVALID_PARAMETER
    • INVALID_JSON
    • INVALID_REQUEST
    • all 39

A breaking change is one that makes a request that used to work stop working, or return something a correct client would misread. It ships under a new prefix; /v1 keeps answering as it did.

  • Removing or renaming a field, an endpoint, a parameter or a vocabulary value.
  • Changing a field's type or unit, or the meaning of a value that already exists.
  • Tightening validation so that a request that used to be accepted is refused.
  • Changing which status code, error code or plan a request answers with.
  • Changing the envelope, the authentication headers or the pagination contract.

The default path

  1. 1The change is filed as a breaking entry in the changelog before it ships, stating what moves and why.
  2. 2The new contract goes live under /v2. The spec's major moves with it, and the SDKs gain a release that targets it.
  3. 3/v1 keeps answering unchanged for at least 12 months from that day. The operations it will lose are marked deprecated in the spec, which the reference and the SDKs surface.
  4. 4When the old prefix is retired, the date is announced in the changelog at least 12 months ahead, and every key that called it inside the usage window is contacted directly.

A new prefix is the right answer for a change of mind. It is the wrong answer for a defect: keeping /v1 wrong for 12 months so that nobody's code changes serves the few callers who depend on the wrong answer and fails everyone else. Where the usage logs show the affected surface has almost no consumers, the fix may land in the current prefix instead. The conditions are narrow and all of them must hold.

When it is allowed

  • The current behaviour is wrong against its own documentation, not merely inconvenient: a figure in the wrong unit, a status that lies, a validation that lets through what the spec forbids.
  • The usage logs for the last 90 days show the affected endpoint, field or value is consumed by almost nobody, and every key that did call it can be contacted directly.
  • The corrected behaviour is what a reader of the documentation already expects, so a client written to the docs works better after the change, not worse.
  • It is a fix, not a feature: nothing new is introduced, and the change is the smallest that makes the behaviour true.

When it is not

  • Any endpoint the SDKs, the website or a published integration pack calls in a documented flow.
  • Any change of a field's name, type or unit on a record: a mirror holding the old shape cannot tell a corrected value from a changed one.
  • Any change to authentication, the envelope, the error contract or pagination.
  • Anything where a consumer would be surprised rather than relieved.

How it ships

  1. 1A breaking entry lands in the changelog and the feed at least 30 days before the change takes effect, naming the endpoint, the old behaviour, the new one and the date.
  2. 2Every key that called the affected surface inside the last 90 days receives the same notice by email, so nobody learns of it from a failing request.
  3. 3The change lands on the announced date with a patch to info.version and a matching SDK release; the changelog entry is updated to say it has shipped.
  4. 4Where a caller cannot move in time, the old behaviour is kept for them behind a per-key exception until they can, so an announced correction never breaks a customer who asked for more time.

Breaking entries published so far (1)

An operation on its way out is marked deprecated in the OpenAPI document, which generated clients and the reference surface. It keeps answering until the retirement date its changelog entry names.

Nothing is deprecated in 1.1.0. Every documented operation is current.

Four places a change is announced. Following one is enough; a breaking change appears in all of them.

The changelog

Every change, filed by category. Filter on breaking to see only what needs your attention.

The feed

https://api.gunspec.io/changelog.xml carries the same entries the moment they publish, for a feed reader or a CI job that fails a build on a new breaking entry.

The OpenAPI document

https://api.gunspec.io/openapi.json carries info.version and every deprecated flag. Diffing it in CI against the copy you built from is the mechanical check; see tools for the Postman and Swagger routes to it.

SDK releases

A new prefix is a major release of the SDKs, an additive change a minor one, so pinning the SDK's major pins the API's.

Five habits that make every compatible change invisible and every breaking change a planned upgrade.

  • Pin the prefix. Build /v1 into the base URL, never into a setting a deploy can change by accident.
  • Ignore what you do not know. Unknown fields, unknown parameters, unknown vocabulary values and unknown error.reasons are all normal; treat them as absent, not as errors.
  • Branch on error.code, then on the reasons you can act on. The error reference lists both.
  • Page from the page you received, not from a total you may not have. The pagination page has the loop.
  • Use an SDK where you can. Its major tracks the prefix, its types track the spec, and its auto-paging and retry already follow these rules.