# Count an audience

> POST /v1/counts. Count an audience, inline or by aud_ id, at contact or company level with up to three breakdown fields.

Source: https://emailmovers.com/mydatahub/docs/reference/rest/create-count

`POST /v1/counts`

Count an audience, inline or by aud_ id, at contact or company level with up to three breakdown fields. Returns Total, New to you (signed in) and Emailable with companies, breakdowns, quality, the compliance summary, a price estimate and a content-addressed count id (also the ETag). Free, and needs no account; anonymous and trial callers see counts under 100 and every breakdown cell rounded to the nearest 5. Lawful basis for named work emails is UK GDPR Article 6(1)(f) legitimate interests; sole traders and ordinary partnerships are individual subscribers and are left out of email audiences by default.

| Property | Value |
| --- | --- |
| Endpoint | `POST https://mydatahub.emailmovers.com/v1/counts` |
| Operation id | `createCount` |
| Cost | Free (0 credits) |
| Access | No account needed. An API key or OAuth token with the scope `counts:read` is optional and adds your organisation's own figures. |
| MCP tool | [`count_audience`](https://emailmovers.com/mydatahub/docs/reference/mcp/count_audience.md) |
| Tag | Counts |

> **Not yet live.** MyDataHub v2 is built and tested locally and is not yet live. The v2 hosts open at launch, date to confirm. This page describes the contract the API is tested against.

## Request

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `MyDataHub-Version` | header | string | no | The dated API version, pinned per key when left out. Non-breaking changes ship as new dates; the path major changes only for breaking changes, with a 12 month deprecation window. |

### Body

| Member | Type | Required | Description |
| --- | --- | --- | --- |
| `audience` | string or `Audience` | yes | An audience definition (mydatahub.audience/1) inline, or the aud_ id of a saved audience. |
| `level` | enum: `contact`, `company` | no | contact (the default) or company. Contacts and companies come back together either way. Default `contact`. |
| `breakdown_by` | array of enum: `company.sic_2007`, `company.business_class`, `company.employee_band`, `company.turnover_band`, `company.fleet_band`, `company.type`, `company.status`, `company.subscriber_class`, and 11 more | no | Up to three fields to break the count down by, each once, such as industry by seniority by region. Returns the top 50 cells per field plus other. |

## Example request

```bash
curl -X POST "https://mydatahub.emailmovers.com/v1/counts" \
  -H "Content-Type: application/json" \
  --data '{
  "audience": {
    "schema": "mydatahub.audience/1",
    "dataset": "uk_contacts",
    "where": {
      "all": [
        {
          "field": "contact.title",
          "op": "matches",
          "terms": ["marketing manager"],
          "mode": "contains"
        },
        {
          "field": "contact.job_level_rank",
          "op": "in",
          "values": [1, 2, 3, 4, 5, 6, 8, 9]
        },
        {
          "field": "company.sic_2007",
          "op": "in",
          "values": ["C"]
        },
        {
          "field": "company.employee_band",
          "op": "gte",
          "value": "50-99"
        },
        {
          "field": "site.location",
          "op": "within_radius",
          "centre": {
            "place": "Leeds"
          },
          "radius_miles": 20
        },
        {
          "not": {
            "field": "email.address_sha256",
            "op": "in_list",
            "list_id": "sup_01JB4H7Q2V7XN3M9R6T1Y5W8ZA"
          }
        }
      ]
    }
  },
  "level": "contact",
  "breakdown_by": ["contact.job_level_rank"]
}'
```

## Responses

### 200: The count.

Headers: `RateLimit`, `RateLimit-Policy`, `ETag`.

| Member | Type | Always present | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Format: a count id: cnt_ followed by 20 base32 characters (A to Z and 2 to 7), such as cnt_7K2PQ4MAXN3R6TBY5WDZ. |
| `dataset` | enum: `uk_contacts`, `ie_contacts`, `global_contacts` | yes | The dataset an audience draws from. uk_contacts is live; ie_contacts and global_contacts are listed but not yet served. |
| `dataset_version` | string | yes | The immutable weekly build a count is bound to, such as 2026-W39. The last 52 stay queryable. |
| `engine_rules_version` | string | yes | The engine rules (title expansion, defaults, geography sets) the count ran under; part of what the count id hashes, such as 2026.09.1. |
| `as_of` | string (date) | no | The date the dataset version was built, for as-of statements. |
| `level` | enum: `contact`, `company` | no | contact (the default) or company. Contacts and companies come back together either way. |
| `credits_per_record` | number (0 or more) | yes | Credits, to one decimal place: whole at 1 credit a record, fractional only where a rule is (0.1 company-only, 1.8 outright, both proposed). |
| `contacts` | integer (0 or more) | yes | Total: every matching contact. |
| `companies` | integer (0 or more) | yes |  |
| `emailable` | integer (0 or more) | yes | A named email scoring 70 or more at a corporate subscriber. |
| `new_to_you` | integer (0 or more) or null | yes | Excludes your earlier orders and CRM overlap; null when signed out. |
| `premium` | integer (0 or more) or null | yes | Premium: the contacts of all_records that are premium records (contact.premium true). At launch, a premium record is in the premium export of a stated date. Null when the dataset version carries no premium data, or withheld when the premium records are at a single company and the caller has no account; premium_note says which. |
| `all_records` | integer (0 or more) | yes | All records: every matching contact with the audience's top-level premium filters taken out (a premium filter inside any or not stays), so the premium figure always has the full figure beside it. The same as contacts when the audience has no premium filter. |
| `premium_note` | string | yes | What premium means on this dataset version, or why premium is null: no premium data on the version, or withheld because the premium records are at a single company. |
| `views` | `CountViews` | no | Contacts and companies for the three headline views, and the premium figure beside all records. |
| `views.total` | `CountView` | yes | Contacts and companies in one headline view. |
| `views.new_to_you` | `CountView` or null | yes | Removes records from earlier orders and a connected CRM; null when signed out. |
| `views.emailable` | `CountView` | yes | A named email scoring 70 or more at a corporate subscriber. |
| `views.premium` | `CountView` or null | yes | The premium records of all_records (contact.premium true); null when the dataset version carries no premium data, or withheld when they are at a single company and the caller has no account. |
| `views.all_records` | `CountView` | yes | The full figure: the audience with its top-level premium filters taken out (one inside any or not stays). The same as total when it has none. |
| `quality` | `Quality` | yes | Quality signals shown with every count. |
| `quality.email_coverage_pct` | number (0 to 100) | yes | Records with a named email as a share of the count. |
| `quality.verified_90d_pct` | number (0 to 100) | yes | Share of emails with a provider result of ok in the last 90 days. |
| `quality.verified_measured_at` | string (date) | no |  |
| `quality.deliverability` | object | no | Score distribution: good 70 to 100, risky 40 to 69, poor under 40. |
| `quality.avg_days_since_verification` | number (0 or more) | no |  |
| `quality.phone_coverage_pct` | number (0 to 100) | no | Share with a validated number, when phone is requested. |
| `quality.tps_ctps_screened_at` | string (date) | no | The version's TPS and CTPS screen date, when phone is requested and a number in it is screened; each number carries its own status and screen date. |
| `breakdowns` | array of `Breakdown` | yes |  |
| `breakdowns[].by` | enum: `company.sic_2007`, `company.business_class`, `company.employee_band`, `company.turnover_band`, `company.fleet_band`, `company.type`, `company.status`, `company.subscriber_class`, and 11 more | yes | A field a count can be broken down by. |
| `breakdowns[].cells` | array of `BreakdownCell` | yes |  |
| `breakdowns[].other` | `OtherBucket` or null | no |  |
| `crosstab` | `Crosstab` or null | no |  |
| `crosstab.by` | array of enum: `company.sic_2007`, `company.business_class`, `company.employee_band`, `company.turnover_band`, `company.fleet_band`, `company.type`, `company.status`, `company.subscriber_class`, and 11 more | yes |  |
| `crosstab.cells` | array of `CrosstabCell` | yes |  |
| `crosstab.other` | `OtherBucket` or null | yes |  |
| `crosstab.truncated` | boolean | yes | True when a cap cut the table: 5,000 cells, or fewer over MCP to keep a result under about 8,000 tokens. |
| `compliance_summary` | `ComplianceSummary` | yes | Beside every count, so a person or an agent can reason before spending. Individual subscribers and TPS and CTPS registered numbers are excluded by default, as visible toggles; numbers not screened are kept. |
| `compliance_summary.lawful_basis` | enum: `legitimate_interests_art6_1_f` | yes |  |
| `compliance_summary.corporate_subscriber` | integer (0 or more) | yes | Records at corporate subscribers. |
| `compliance_summary.individual_subscriber_excluded` | integer (0 or more) | yes | Sole traders and ordinary partnerships left out of an email audience. |
| `compliance_summary.tps_ctps_excluded` | integer (0 or more) | yes | TPS or CTPS registered numbers left out when phone is requested. |
| `compliance_summary.article14_notified` | integer (0 or more) | yes |  |
| `compliance_summary.article14_pending_within_window` | integer (0 or more) | yes | Records inside the one month Article 14 window, not yet notified. |
| `compliance_summary.suppressed_removed` | array of `SuppressionRemoval` | yes |  |
| `compliance_summary.jurisdiction_excluded` | array of `JurisdictionExclusion` | no | Partner datasets only: records the jurisdiction policy table left out (Germany, Canada and California by policy), reported like suppression removals. |
| `defaults` | array of `CountDefault` | yes | Every default with the number behind its toggle: dissolved and insolvent, generic emails, individual subscribers, TPS and CTPS registered. |
| `defaults[].id` | enum: `active_companies`, `named_emails`, `corporate_subscribers`, `no_registered_phones` | yes | active_companies, named_emails, corporate_subscribers or no_registered_phones. |
| `defaults[].applied` | boolean | yes | False when the audience filters the same field itself, or turned it off. |
| `defaults[].views` | array of enum: `total`, `new_to_you`, `emailable` | yes | The headline views the default shapes; named emails and corporate subscribers shape Emailable only. |
| `defaults[].removed` | integer (0 or more) or `under 10` or `suppressed` | yes | What the toggle shows: the dissolved and insolvent count, the generic (info@, sales@) count, the individual subscriber count or the TPS and CTPS registered count. |
| `defaults[].screened_at` | string (date) or null | no | no_registered_phones only: the version's TPS and CTPS screen date. |
| `estimate` | `PriceEstimate` | yes | What ordering the whole count would cost. An estimate; quote_order gives the machine quote. |
| `estimate.credits` | number (0 or more) | yes | Credits, to one decimal place: whole at 1 credit a record, fractional only where a rule is (0.1 company-only, 1.8 outright, both proposed). |
| `estimate.gbp_ex_vat` | map of number (0 or more) | yes | The price in GBP excluding VAT: per_contact_scale (every credit at the scale) and cheapest (the cheapest real purchase, at the scale or with credit packs). |
| `expanded_terms` | array of `ExpandedTerm` | no | Every title a matches filter covered, with a count per term. |
| `expanded_terms[].term` | string | yes | A matched title. |
| `expanded_terms[].from_term` | string | yes | The input term it came from. |
| `expanded_terms[].expansion_set` | enum: `plural_singular`, `department_synonyms`, `ampersand_and`, `abbreviations`, `permutations` or `input` | yes | The expansion set that produced it, or input for the term itself. |
| `expanded_terms[].contacts` | integer (0 or more) or `under 10` or `suppressed` | yes | A whole number; "under 10" when disclosure control withholds a small cell; or "suppressed" when secondary suppression hides a cell of 10 or more in a cross-tab or breakdown, so totals cannot be differenced. Anonymous and trial callers see cells rounded to the nearest 5. |
| `expanded_terms[].excluded` | boolean | no | True when exclude_terms removed it from the audience. |
| `disclosure` | `Disclosure` | yes | Whether these numbers are exact or protected: "under 10" cells, rounding to 5 and secondary suppression for anonymous and trial callers. |
| `disclosure.level` | enum: `exact`, `rounded` | yes | exact for authenticated paid callers; rounded for anonymous and trial callers, whose counts under 100 and every cell are rounded to the nearest 5. |
| `disclosure.rounded_to` | integer or null | yes | 5 when numbers are rounded; null when they are exact. |
| `disclosure.secondary_suppression` | boolean | yes | True when cross-tab or breakdown cells of 10 or more were hidden as "suppressed" so totals cannot be differenced. |
| `disclosure.notes` | array of string | yes |  |
| `effective_audience` | `Audience` | no | The definition after defaults and bundles, as hashed into the id. See [Audience](https://emailmovers.com/mydatahub/docs/audience.md). |
| `plan` | `CompiledPlan` | no | How the definition was run: expanded terms, resolved centres, SIC codes behind a business class and suppression checksums. |
| `plan.engine_rules_version` | string | yes |  |
| `plan.defaults_applied` | array of string | yes | Defaults the server filled, such as active_companies and named_emails. |
| `plan.expanded_terms` | array of `ExpandedTerm` | yes |  |
| `plan.centres` | array of `ResolvedCentre` | yes |  |
| `plan.sic_codes` | array of string | yes | SIC codes behind any business class filter. |
| `plan.geography_sets` | array of string | yes |  |
| `plan.suppressions` | array of `SuppressionChecksum` | yes |  |
| `current` | `CurrentCount` or null | no | On a replay: the same audience on the current dataset version. |
| `current.dataset_version` | string | yes | The immutable weekly build a count is bound to, such as 2026-W39. The last 52 stay queryable. |
| `current.contacts` | integer (0 or more) | yes |  |
| `current.companies` | integer (0 or more) | yes |  |
| `removed_since` | integer (0 or more) | no | On a replay: records since removed by objections and erasure. |

Showing the first 80 of 83 members. The OpenAPI document and the MCP tool list hold the full schema.

#### Example response

```json
{
  "id": "cnt_7K2PQ4MAXN3R6TBY5WDZ",
  "dataset": "uk_contacts",
  "dataset_version": "2026-W39",
  "credits_per_record": 1,
  "contacts": 12840,
  "companies": 3115,
  "emailable": 12310,
  "new_to_you": null,
  "quality": {
    "email_coverage_pct": 96.8,
    "verified_90d_pct": 88.4
  },
  "breakdowns": [
    {
      "by": "contact.job_level_rank",
      "cells": [
        {
          "value": "director",
          "contacts": 4120
        }
      ]
    }
  ],
  "compliance_summary": {
    "lawful_basis": "legitimate_interests_art6_1_f",
    "corporate_subscriber": 12310,
    "individual_subscriber_excluded": 530,
    "tps_ctps_excluded": 1120,
    "article14_notified": 12290,
    "article14_pending_within_window": 20,
    "suppressed_removed": [
      {
        "list_id": "sup_01JB4H7Q2V7XN3M9R6T1Y5W8ZA",
        "removed": 260
      }
    ]
  },
  "estimate": {
    "credits": 12310,
    "gbp_ex_vat": {
      "per_contact_scale": 2708.2,
      "cheapest": 1249
    }
  },
  "expires_at": "2027-09-26T10:14:00Z",
  "engine_rules_version": "2026.09.1",
  "defaults": [
    {
      "id": "active_companies",
      "applied": true,
      "views": ["total", "new_to_you", "emailable"],
      "removed": 2210
    },
    {
      "id": "named_emails",
      "applied": true,
      "views": ["emailable"],
      "removed": 410
    },
    {
      "id": "corporate_subscribers",
      "applied": true,
      "views": ["emailable"],
      "removed": 530
    },
    {
      "id": "no_registered_phones",
      "applied": false,
      "views": ["total", "new_to_you", "emailable"],
      "removed": 1120,
      "screened_at": "2026-09-25"
    }
  ],
  "disclosure": {
    "level": "rounded",
    "rounded_to": 5,
    "secondary_suppression": false,
    "notes": [
      "Counts under 100 and every breakdown cell are rounded to the nearest 5 for anonymous and trial callers."
    ]
  },
  "premium": 8120,
  "all_records": 12840,
  "premium_note": "Premium at launch: in the premium export of 2026-09-28, this dataset version's as-of date."
}
```

## Errors

Every error is a problem object with a `code` and a `next_step`. Every response carries the `RateLimit` and `RateLimit-Policy` headers; see [Rate limits](https://emailmovers.com/mydatahub/docs/rate-limits.md).

| Status | Problem codes | When |
| --- | --- | --- |
| 400 | [`invalid-request`](https://emailmovers.com/mydatahub/docs/errors/invalid-request.md), [`idempotency-key-required`](https://emailmovers.com/mydatahub/docs/errors/idempotency-key-required.md), [`unsupported-version`](https://emailmovers.com/mydatahub/docs/errors/unsupported-version.md) | The request could not be read, or an Idempotency-Key is missing. |
| 401 | [`authentication-required`](https://emailmovers.com/mydatahub/docs/errors/authentication-required.md), [`account-required`](https://emailmovers.com/mydatahub/docs/errors/account-required.md) | Sign in (OAuth) or send an API key (authentication-required), or create a free account for what the anonymous tier does not cover (account-required). |
| 422 | [`validation-failed`](https://emailmovers.com/mydatahub/docs/errors/validation-failed.md), [`quote-required`](https://emailmovers.com/mydatahub/docs/errors/quote-required.md), [`dataset-unavailable`](https://emailmovers.com/mydatahub/docs/errors/dataset-unavailable.md) | The request is not valid. errors lists every problem with a JSON pointer and did_you_mean. |
| 429 | [`rate-limited`](https://emailmovers.com/mydatahub/docs/errors/rate-limited.md) | Too many requests. Wait for Retry-After. |
| Any other failure | [`internal-error`](https://emailmovers.com/mydatahub/docs/errors/internal-error.md), [`service-unavailable`](https://emailmovers.com/mydatahub/docs/errors/service-unavailable.md), [`tps-screen-expired`](https://emailmovers.com/mydatahub/docs/errors/tps-screen-expired.md) | Something failed on our side (500 internal-error) or the service is briefly unavailable (503 service-unavailable, with Retry-After). Nothing was charged; retry with the same Idempotency-Key. A count, sample, quote or order with telephone in play is also refused with 503 tps-screen-expired, with no Retry-After, while the dataset version's TPS and CTPS screen is more than 28 days old. |
