Skip to main content
Adopt

IPA-120: Versioning

API producers should avoid incrementing the API version where possible, prioritizing supporting the current API version unless absolutely necessary. This philosophy reduces exposing clients to the friction of migrating versions, and multiple versions complicate maintenance, testing, and documentation.

Guidance

  1. An API producer should consider whether a change warrants a new version or can instead be made in a backward-compatible fashion, referring to backward compatibility (IPA-116) before versioning the API.

    paths:
    /users/{userId}:
    get:
    operationId: getUser
    responses:
    "200":
    content:
    application/json:
    schema:
    type: object
    properties:
    id:
    type: string
    name:
    type: string
    email:
    type: string
    Why:

    Adding the optional email field to an existing response is additive and backward compatible, so the change lands in the current version instead of forcing a new one.

    paths:
    /v2/users/{userId}:
    get:
    operationId: getUserV2
    responses:
    "200":
    content:
    application/json:
    schema:
    type: object
    properties:
    id:
    type: string
    name:
    type: string
    email:
    type: string
    Why:

    A whole new version is spun up to carry one additive field that the current version could already accept, splitting clients across versions for no compatibility reason.

    1. Identify the change under review and the version it targets — a new version path/prefix, a new version-tagged operation, or a header-selected version.

    2. Classify the change against IPA-116: adding optional fields, new optional parameters, or new operations is additive; removing or renaming fields/paths, tightening types, or changing required-ness is breaking.

    3. Confirm that any change introducing a new version is one that cannot be expressed additively in the current version.

    4. Report any new version whose change is additive and could have stayed in the current version.

  2. An API producer should not version the API when the desired functionality can be achieved in a backward-compatible fashion.

    paths:
    /orders/{orderId}:
    get:
    operationId: getOrder
    parameters:
    - name: includeItems
    in: query
    required: false
    schema:
    type: boolean
    responses:
    "200":
    description: An order.
    Why:

    The new behavior is opt-in through an optional query parameter on the existing operation, so the functionality ships without a version bump.

    paths:
    /orders/{orderId}:
    get:
    operationId: getOrder
    responses:
    "200":
    description: An order.
    /v2/orders/{orderId}:
    get:
    operationId: getOrderV2
    parameters:
    - name: includeItems
    in: query
    required: false
    schema:
    type: boolean
    responses:
    "200":
    description: An order with items.
    Why:

    The same opt-in behavior is delivered by duplicating the operation under a new version, even though an optional parameter on the existing operation would have been backward compatible.

    1. For each newly introduced version, list the functionality it adds relative to the prior version.

    2. For each added capability, determine whether it could be expressed in the current version through additive means: an optional parameter, an optional response field, or a new operation.

    3. Flag any version whose entire delta is achievable backward-compatibly in the current version.

  3. An API producer should consider introducing a new endpoint alongside the existing one, in lieu of a new version.

    paths:
    /projects/{projectId}/reports:
    get:
    operationId: listReports
    /projects/{projectId}/reports/summary:
    get:
    operationId: getReportSummary
    Why:

    The new summarized behavior is exposed as an additional endpoint next to the existing collection, so neither the operation nor the version has to change.

    paths:
    /projects/{projectId}/reports:
    get:
    operationId: listReports
    /v2/projects/{projectId}/reports:
    get:
    operationId: listReportsV2
    Why:

    A distinct capability is delivered by re-versioning the same resource rather than adding a sibling endpoint, which would have left existing consumers untouched.

    1. For each new version, identify the resources or operations it introduces.

    2. Determine whether each new capability represents distinct functionality that could live at its own path alongside the existing endpoints.

    3. Flag versions whose additions are new capabilities that a sibling endpoint in the current version could have carried.

  4. An API producer should avoid versioning an API solely to rename existing fields or paths.

    paths:
    /invoices/{invoiceId}:
    get:
    operationId: getInvoice
    responses:
    "200":
    content:
    application/json:
    schema:
    type: object
    properties:
    total:
    type: number
    amount:
    type: number
    deprecated: true
    Why:

    A clearer name is added while the old field is kept and marked deprecated, so the rename happens without a version bump and without breaking existing clients.

    paths:
    /v2/invoices/{invoiceId}:
    get:
    operationId: getInvoiceV2
    responses:
    "200":
    content:
    application/json:
    schema:
    type: object
    properties:
    total:
    type: number
    Why:

    A new version exists only to rename amount to total, which forces every client to migrate for a purely cosmetic change.

    1. For each new version, diff its fields and paths against the prior version.

    2. Determine whether the only differences are renamed fields or paths with no change in behavior, type, or semantics.

    3. Flag any version whose sole change is a rename of existing fields or paths.

  5. An API producer may coordinate to sunset old versions faster than the default timeline of one year.

  6. An API producer must coordinate with clients to transition to new versions to avoid impact.

    paths:
    /v1/users/{userId}:
    get:
    operationId: getUserV1
    deprecated: true
    responses:
    "200":
    headers:
    Sunset:
    schema:
    type: string
    format: date
    description: A user. This version is deprecated; migrate to v2.
    /v2/users/{userId}:
    get:
    operationId: getUserV2
    responses:
    "200":
    description: A user.
    Why:

    The superseded version stays available, is marked deprecated, and advertises a Sunset date, giving consumers a signalled window to move before the old version is withdrawn.

    paths:
    /v2/users/{userId}:
    get:
    operationId: getUserV2
    responses:
    "200":
    description: A user.
    Why:

    The previous version was removed outright with no deprecation marker or sunset signal, so consumers still on it break the moment it disappears.

    1. Compare the spec against the previously published version to find any operations or versions that were removed or replaced.

    2. For each superseded version, confirm the prior version remains available during a transition window rather than being dropped at once.

    3. Confirm the superseded operations are marked deprecated and advertise a sunset signal (such as a Sunset header or a deprecation date in the description).

    4. Inspect release notes, changelogs, or client-communication records to confirm consumers were notified ahead of the transition.

    5. Report any version transition that removes a prior version without a deprecation period, sunset signal, and evidence of client coordination.

Further Reading