# Versioning and changes

URL: /docs/concepts/versioning

How the API is versioned, which changes we treat as breaking and which as additive, how to write clients that keep working, and where changes are announced.

The API is versioned in its path, and the version only changes for breaking changes. Within a version, we add to the API without taking anything away. This page explains what that means for your code.

## The version is in the path [#the-version-is-in-the-path]

Every endpoint lives under `/api/v1`:

```
https://api.elaraone.app/api/v1/products
```

There is no version header to send. The current version is `v1`.

## How we treat changes [#how-we-treat-changes]

**Additive changes** can ship at any time within `v1`:

- new endpoints;
- new optional query parameters, request fields and headers;
- new fields in responses, including new fields inside existing objects;
- new keys in `meta` and `details`;
- new error codes, for new situations;
- new values in lists that are documented as open, such as `decline_code` on a declined payment;
- new event types for webhooks you can subscribe to.

**Breaking changes** are the ones we avoid within `v1`. We treat these as breaking:

- removing or renaming an endpoint, a field, a parameter or a header;
- changing a field's type or the meaning of a value;
- making an optional parameter or field required;
- changing which `error.code` an existing situation returns.

When a security change has to tighten what existing credentials can do, it is phased in rather than switched on overnight. The storefront API lock is an example: each store ran in report-only mode first, with the calls that would be refused shown in the admin, before the lock was enforced. See [stores and keys](/docs/concepts/stores-and-keys#report-only-then-enforcing).

## Write clients that keep working [#write-clients-that-keep-working]

Because additive changes can arrive at any time, build clients that tolerate them:

- **Ignore fields you do not know.** Do not fail on unexpected keys, and do not use strict schemas that reject them.
- **Treat unknown values as a safe default.** An unknown `decline_code` is `unknown`; an unknown order `status` should still show `status_label`; an unknown promotion `kind` can be shown by its `label`.
- **Handle unknown error codes.** Fall back on the HTTP status and show `error.message`.
- **Do not depend on the order of keys** in an object, or on the order of groups in search suggestions (use the order the search config gives you).
- **Read optional fields defensively.** Fields documented as nullable can be `null`.

```ts title="Tolerant parsing"
type DeclineCode =
  | 'card_declined' | 'card_expired' | 'insufficient_funds'
  | 'incorrect_card_details' | 'authentication_required' | 'try_another_method' | 'unknown'

const known = new Set<DeclineCode>([
  'card_declined', 'card_expired', 'insufficient_funds',
  'incorrect_card_details', 'authentication_required', 'try_another_method', 'unknown',
])

export function declineCode(value: unknown): DeclineCode {
  return typeof value === 'string' && known.has(value as DeclineCode) ? (value as DeclineCode) : 'unknown'
}
```

## Webhook payload versions [#webhook-payload-versions]

CRM change webhooks carry an `api_version` in every body, so a receiver can tell which payload shape it is reading:

```json title="Webhook body (trimmed)"
{
  "id": "01JA5W9Q3V8K2YB7N6T4M0H1XC",
  "type": "contact.updated",
  "created_at": "2026-10-13T09:30:00+00:00",
  "store": "hearth-and-oak",
  "api_version": "2026-10-01",
  "data": { "object": "contact", "id": 12, "changed": ["email", "lifecycle_stage"], "actor": { "type": "api" } }
}
```

The current payload version is `2026-10-01`. Read it to tell which payload shape you are handling. New fields and new event types can still be added under the same version, so ignore what you do not recognise.

## Deprecations you can detect [#deprecations-you-can-detect]

Some changes are also signalled in responses. For example, after a store key is rotated, responses to calls made with the old key carry `X-Tenant-Key-Deprecated: true` until the overlap ends. Log headers like this in your integration so they show up before anything stops working. See [stores and keys](/docs/concepts/stores-and-keys#rotate-the-public-key).

## The changelog [#the-changelog]

Changes to the API are announced in the [changelog](/changelog). Check it when you upgrade a starter storefront or an integration.
