# Versions and reproducibility

> A count id re-runs exactly for 12 months on its pinned dataset version. The API evolves by dated version, and the path major changes only for breaking changes.

Source: https://emailmovers.com/mydatahub/docs/versions
Updated: 4 October 2026

Three things are versioned, and each is pinned so a result can be reproduced. The API has a dated `MyDataHub-Version`, the data has an immutable weekly `dataset_version`, and the counting rules have an `engine_rules_version`. A count id hashes the last two, so the same definition on the same data gives the same id for 12 months.

> **Not yet live.** MyDataHub v2 is built and tested locally and is not yet live. This page describes the contract the API is tested against, and the launch date is to confirm. The Monday build schedule and the Companies House streams are planned and not built.

## API versions

The base is `https://mydatahub.emailmovers.com/v1`. Within `/v1`, behaviour changes by date.

- Send `MyDataHub-Version: 2027-01-15` to pin a version. Leave it out and your key's `api_version` applies.
- Non-breaking changes ship as a new date. An unsupported date answers [unsupported-version](https://emailmovers.com/mydatahub/docs/errors/unsupported-version.md).
- The path major changes only for a breaking change, and the old major stays available for 12 months. There are no `/v2` paths.
- Response objects are open. A new member may appear in a later version, so ignore members you do not know. Request bodies are closed and refuse unknown members.
- A webhook endpoint is pinned to the version of the request that created it, and every event carries its `api_version`.

`2027-01-15` is the only version so far, and it is the one these docs describe. Lists use cursor pagination and return `{data, has_more, next_cursor}`, with `limit` defaulting to 25 and a maximum of 100 (500 on records).

## Dataset versions

Every Monday the pipeline builds an immutable `dataset_version`, such as `2026-W39`, and flips the current pointer. The last 52 stay queryable, so any count from the last 12 months re-runs exactly.

The plan is for Companies House changes to land within 15 minutes through its streaming feeds and to appear in the next build. The streams are not built: the build reads Companies House data as files.

Opt-outs, objections and erasure requests do not wait for Monday. A same-day overlay is subtracted from every count and delivery. A count replayed on its pinned version therefore reports `removed_since` beside the original number.

`GET /v1/datasets` lists each dataset with its owner, source, licence, refresh, residency, regulator, warranty and `credits_per_record`. Version 2.0 serves `uk_contacts` only. `ie_contacts` and `global_contacts` are listed but not yet served, and asking for them answers [dataset-unavailable](https://emailmovers.com/mydatahub/docs/errors/dataset-unavailable.md).

Each version publishes what changed at `GET /v1/datasets/uk_contacts/versions/2026-W39`.

| Member | What it holds |
| --- | --- |
| `row_counts` | Companies, sites, contacts, emails and phones in the build |
| `changes` | Records added, retired and re-verified, companies dissolved, job changes confirmed and suppressions applied |
| `coverage_pct` | Coverage by field |
| `input_snapshots` | The source snapshots the build used, with their dates |
| `checksum` | A checksum of the build |

## How a count id is made

A count id is content addressed. It is `cnt_` followed by the first 20 base32 characters of the SHA-256 of three things in [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) canonical JSON:

1. The effective definition, after the server fills its defaults and puts it in canonical form.
2. The `dataset_version`.
3. The `engine_rules_version`.

The canonical form sorts and deduplicates `in` and `since` values, terms and expansion sets, flattens nested groups of the same kind, unwraps a group of one and a double `not`, and upper cases postcodes. A bundle is replaced by its fragment.

The definition reaches the same id from chips, REST or MCP, so there is one id and one cache entry. The id is the same for every caller, level and breakdown.

## Replay a count

`GET /v1/counts/{id}` returns the count as it ran, worked out again for the caller's own tier, so an anonymous caller who knows a paid caller's id sees rounded numbers. It returns:

- the effective definition and the compiled plan, with the expanded terms, the resolved centroid and cells, and the SIC codes behind a business class,
- the pinned `dataset_version` and `engine_rules_version`,
- all three views, the breakdowns, the compliance summary and the quality block,
- `removed_since`, the Total as it ran less the Total now, and
- `expires_at`, 12 months after the count ran.

The `ETag` equals the id, so a client that holds a count can ask with `If-None-Match` and get `304`. A count that read your organisation's private suppression lists belongs to your organisation, and anyone else gets 404.

## Quotable counts

Because a count replays, it can be cited. The app shows each count at `mydatahub.emailmovers.com/counts/{id}` with a "Cite this count" button.

It copies a line such as "Source: MyDataHub by Emailmovers, 1,605,275 named UK business contacts, as of 26 September 2026" and the count URL. We never put a count or a price behind sign-up.

## Reproducibility checklist

- Save the `id` of every count you rely on.
- Keep the `dataset_version` and `engine_rules_version` it returned.
- Quote against the count id, so the quote is bound to the same dataset version.
- Re-run with `GET /v1/counts/{id}` within 12 months, and read `removed_since`.

## Next steps

- Count with [Counts and breakdowns](https://emailmovers.com/mydatahub/docs/counts.md).
- Read what changed and what is going in [Changelog and deprecation policy](https://emailmovers.com/mydatahub/docs/changelog.md).
- See the headers and the cache rules in [Rate limits](https://emailmovers.com/mydatahub/docs/rate-limits.md).
