EmailmoversMyDataHub docs
Browse the docs

Guide

The audience definition

Every count starts from one JSON document in the namespace mydatahub.audience/1: filters in groups, with fields, operators, title expansion, geography and defaults.

Updated 17 October 2026View as Markdown

An audience is one JSON document, and the app's chips, a brief compiled by an agent and a saved audience (aud_) all produce the same one. It has 6 members, 32 fields in 6 groups and 11 operators, and the same document gives the same count over REST, MCP and in the app. The JSON Schema is at /reference/audience.schema.json.

A worked example

This audience is "marketing managers and above at manufacturing companies with 50 or more staff within 20 miles of Leeds, excluding my suppression list".

{
  "schema": "mydatahub.audience/1",
  "dataset": "uk_contacts",
  "dataset_version": "2026-W39",
  "where": {
    "all": [
      { "field": "contact.title", "op": "matches", "terms": ["marketing manager"], "mode": "contains",
        "expand": ["plural_singular", "department_synonyms", "ampersand_and", "abbreviations"] },
      { "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" } }
    ]
  },
  "email_policy": { "min_deliverability_score": 70, "allow_catch_all": false },
  "dedupe": "none"
}

Read it top to bottom. The all group is an AND of six nodes.

  1. A job title with its expansions.
  2. The seniority ranks for Manager and above.
  3. The SIC section for manufacturing.
  4. 50 or more employees.
  5. A 20 mile radius around Leeds.
  6. A not that removes your suppression list.

Send it to POST /v1/counts as audience, or save it with POST /v1/audiences.

Two rules apply when you send this example as written. Leeds is ambiguous, so the engine refuses it with 422 validation-failed and the candidate ONS codes in did_you_mean. Pin one with code, such as { "place": "Leeds", "code": "E08000035" } for the metropolitan district.

Free text job titles, contact.title with matches, also need a free account. An anonymous request that uses one gets 401 account-required.

Members

An audience has these top-level members. Only schema is required, and parsing never fills a default.

MemberTypeRequiredDescription
schemamydatahub.audience/1yesThe definition's namespace and version. Older versions are upgraded by a pure function.
datasetenum: uk_contacts, ie_contacts, global_contactsnoThe dataset an audience draws from. uk_contacts is live; ie_contacts and global_contacts are listed but not yet served.
dataset_versionstringnoThe weekly build to count against, such as 2026-W39. Left out, the current version is filled in and returned.
whereWhereNodenoThe filters. Left out, the audience is the whole dataset after the defaults. See WhereNode.
email_policyEmailPolicynoEmail quality floor. Defaults: min_deliverability_score 70, allow_catch_all false.
email_policy.min_deliverability_scoreinteger (0 to 100)noLowest deliverability score counted, 0 to 100. Default 70.
email_policy.allow_catch_allbooleannoWhether catch-all addresses count. Default false.
dedupeenum: none, one_per_company, one_per_sitenonone keeps every matching contact, one_per_company keeps one contact per company, one_per_site keeps one per site. Default none.

level and breakdown_by belong to the count request, not the audience. dedupe takes none, one_per_company or one_per_site.

Groups and filters

A node in where is a group or a filter. A group is all (AND), any (OR) or not (negation), and groups nest.

A tree may nest at most 32 groups deep and hold at most 1,000 nodes, so a malformed body gets a 422 problem, never a crash. A filter has a field, an op and the operands that op takes.

OperatorApplies toMeaning
allgroupsAND: every node inside must match. Nests to any depth.
anygroupsOR: at least one node inside must match. Nests to any depth.
notgroupsNegation: the single node inside must not match.
incoded fieldsOne of the listed codes.
betweenbanded and ranked fieldsInclusive range from one value to another over an ordered code list or a number.
gtebanded and ranked fieldsThe value and everything after it in the order (for bands, larger bands).
ltebanded and ranked fieldsThe value and everything before it in the order. For seniority, rank 1 is the most senior, so and above is lte.
matchescontact.titleKeyword match with expansion sets and a mode: contains (the default), starts_with or exact.
within_radiussite.locationWithin radius_miles (1 to 100) of a resolved centre: a place, a postcode or a latitude and longitude.
in_listhashed email, domain, phone, company number and title termMembership of an uploaded suppression list, named by its sup_ id.
sincedate fields and signal.typeWithin the last N days.

Each operator takes its own operands.

OperatorOperands
invalues, a list of codes
gte, ltevalue, a code or number
betweenfrom and to, inclusive
matchesterms, mode, expand and exclude_terms
within_radiuscentre and radius_miles
in_listlist_id, a sup_ id
sincedays, 1 to 3,650, and values for signal.type

A boolean field such as email.exists uses in with [true] or [false], because there is no equality operator.

Fields

Fields are grouped by entity: company., site., contact., email., phone. and signal.. The catalogue is public. GET /v1/catalogue/filters lists every field with its type, operators and value count, and GET /v1/catalogue/filters/{dimension}/values?q= searches the values of one field with a contact and company count for each. The list_filters and search_filter_values tools do the same.

FieldLabelOperatorsBreakdownValues
company.sic_2007Industry (SIC 2007)inyesa SIC 2007 section letter A to U or a code of 2 to 5 digits, such as C, 62 or 62012
company.business_classBusiness classinyesa business class code from the catalogue, such as E2935 (Accountants)
company.employee_bandEmployeesin, between, gte, lteyes13 codes, 1-5 to 10000+ (ordered)
company.turnover_bandTurnoverin, between, gte, lteyes17 codes, 1-100k to 1B+ (ordered)
company.fleet_bandFleet sizein, between, gte, lteyes7 codes, 1-2 to 100+ (ordered)
company.typeCompany typeinyesa company type code in lower case with underscores, such as ltd or charitable_incorporated_organisation
company.statusCompany statusinyesa company status code in lower case with underscores, such as active or dissolved
company.subscriber_classSubscriber classinyes9 codes, ltd to unknown
company.companies_house_numberCompanies House numberin, in_listnoan eight character Companies House number in upper case, such as 05046417 or SC123456
site.regionRegioninyes13 codes, north_east to crown_dependencies
site.countyCountyinyesa county code from the catalogue, such as west_yorkshire
site.townTowninyesa town code from the catalogue, such as leeds
site.postcodePostcodeinyesa postcode area, district, sector or unit, such as LS, LS1, LS1 4 or LS1 4AP
site.locationRadiuswithin_radiusnoa place, postcode or latitude and longitude
contact.titleJob titlematches, in_listnofree text, up to 200 characters
contact.title_familyJob roleinyesa canonical job title code from the catalogue, such as marketing_manager
contact.job_functionJob functioninyesa job function code from the catalogue, such as marketing or finance
contact.job_level_rankSeniorityin, between, gte, lteyes13 codes, 1 to 13 (ordered)
contact.premiumPremiuminnotrue or false
contact.last_validated_atValidation recencysincenodays (1 to 3,650)
contact.company_contact_rankContacts per companyltenowhole number 1 to 1000
email.existsHas emailinyestrue or false
email.typeEmail typeinyes3 codes, named to generic_domain
email.deliverability_scoreDeliverability scoregte, lte, betweennowhole number 0 to 100
email.last_verified_atVerification recencysincenodays (1 to 3,650)
email.address_sha256Email suppressionin_listnoSHA-256 hash in a suppression list
email.domain_sha256Domain suppressionin_listnoSHA-256 hash in a suppression list
phone.existsHas phoneinyestrue or false
phone.tps_ctps_statusTPS and CTPSinyes5 codes, clear to not_screened
phone.e164_sha256Phone suppressionin_listnoSHA-256 hash in a suppression list
signal.typeCompany signalssinceno7 codes, company.incorporated to company.confirmation_statement_overdue
signal.observed_atAny recent signalsincenodays (1 to 3,650)

A field marked for breakdown can also appear in breakdown_by. See Counts and breakdowns.

Job titles

contact.title with matches is a keyword match. Title expansion is data, not code: a dictionary of normalised titles and a synonyms table keyed by expansion set. The server returns the expanded list with a count per term, from POST /v1/job-titles/expand and with every count, so nothing is hidden behind a toggle.

The mode is contains by default, starts_with narrows it, and exact takes a verbatim list with no expansion. The expand member names the sets to apply.

Left out, the first four apply. An empty list applies none, and permutations is opt in because it broadens most.

SetWhat it matchesExamples
plural_singularMatches the singular and plural of each word.manager, managers
department_synonymsMatches the other names for a department.HR, Human Resources, People; IT, Information Technology
ampersand_andTreats an ampersand (&) and the word "and" as the same.Sales & Marketing, Sales and Marketing
abbreviationsMatches an abbreviation and its full form.CEO, Chief Executive Officer; MD, Managing Director
permutationsMatches the same title with its words in another order.Marketing Director, Director of Marketing

Remove a term by adding it to exclude_terms. The exclusion is written into the definition, so the edited count stays reproducible when the title dictionary changes. See Versions and reproducibility.

Seniority and bundles

contact.job_level_rank runs from 1 (Owner) to 13 (Volunteer). Rank 1 is the most senior, so "and above" is lte. A contact with no known level has rank 0, Level not known: only in with 0 matches it, so no range and no "and above" includes it, and its records and samples show no level.

RankLevelSlug
1Ownerowner
2Partnerpartner
3CxOcxo
4Chairchair
5VPvp
6Directordirector
7Company secretarycompany_secretary
8Senior managersenior_manager
9Managermanager
10Senior staffsenior_staff
11Junior staffjunior_staff
12Traineetrainee
13Volunteervolunteer
0Level not knownnot_known

A bundle is a named shortcut written as { "bundle": "sdm" }. The server swaps it for its fragment before the count id is hashed, so the effective definition holds the fragment and not the name.

BundleResolves toStatus
sdmOwner to Director: job level ranks 1 to 6. The 360 SDM bundle.to confirm
manager_and_aboveJob level ranks 1 to 6, 8 and 9. Excludes Company secretary (rank 7), matching the 1,277,443 count of 26 September 2026.confirmed
owner_managedOwners and partners (ranks 1 and 2) at companies with fewer than 50 employees. Proposed.proposed

Geography

Every site carries ONS codes for region, county, local authority, ward, LSOA and ITL levels, plus latitude, longitude and an H3 cell, from the quarterly ONS Postcode Directory under the Open Government Licence v3.0.

  • Region, county and town. Use in with codes from the catalogue. The Crown Dependencies are one region value.
  • Postcode. Use in with a hierarchical code: area LS, district LS1, sector LS1 4 or unit LS1 4AP. A sector needs its space, because LS14 is the district LS14. There is no starts_with operator on postcodes.
  • Radius. within_radius takes a centre and radius_miles from 1 to 100. The centre is exactly one of { "place": "Leeds" }, { "postcode": "M1 1AE" } or { "lat": 53.8, "lon": -1.55 }. A radius under 5 miles needs a free account.

An ambiguous place name such as Leeds comes back from POST /v1/locations/resolve as candidates, never as a silent choice. Pin one with { "place": "Leeds", "code": "..." } using the ONS code from the candidate. London (33 boroughs including the City of London) and Yorkshire (North, South and West Yorkshire and the East Riding) remain as named geography sets.

Premium records

contact.premium keeps premium records: a contact at an active company with a LinkedIn profile, at launch in the premium export of a stated date (the dataset version's as-of date), and later validated within the last 12 months from the validations feed. Use in with [true] for premium records only or [false] for the rest.

{ "field": "contact.premium", "op": "in", "values": [true] }

Two more fields come from the same data. contact.last_validated_at with since keeps records validated in the last N days. contact.company_contact_rank with lte keeps at most that many contacts per company, most senior first and Level not known last; the rank is set across the whole dataset version, so other filters can leave a company with fewer. The server never applies either one for you.

Premium is the default view in the app and in a compiled brief, never in a count: an audience you send is counted exactly as written. Every count shows the premium figure beside all records, so turning premium on or off never hides the other number. See Counts and breakdowns.

A dataset version without premium data cannot answer these fields. A filter on one gets 422 validation-failed with the reason, and GET /v1/catalogue/filters leaves it out, so a count is never 0 for want of data.

Suppressions inside an audience

in_list removes the records on one of your lists. Each list has a kind that must match the field: hashed email on email.address_sha256, domain on email.domain_sha256, phone on phone.e164_sha256, company number on company.companies_house_number and title term on contact.title.

Every count reports how many records each list removed in compliance_summary.suppressed_removed. See Suppressions and objections.

Defaults the server fills

You do not have to ask for these. The server applies each one, writes it into the effective definition it returns, and shows it as a toggle with the number it removed.

DefaultWhat it doesApplies to
Active companiescompany.status is activeEvery view
Named emailsemail.type is named, not info@Emailable
Corporate subscribersSole traders and ordinary partnerships are left out of email audiencesEmailable
No registered numbersNumbers a screen found TPS or CTPS registered are excluded when a phone is requested; numbers not screened stay inEvery view
Your suppressionsEvery list on your account is appliedEvery view

The server also fills the current dataset_version and the email policy: a deliverability score of at least 70, with catch-all addresses excluded. Older schema versions are upgraded by a pure function, so an audience saved in 2026 still compiles in 2028.

More examples

New incorporations in London, at senior decision makers with an email, using a bundle and a signal.

{
  "schema": "mydatahub.audience/1",
  "dataset": "uk_contacts",
  "where": {
    "all": [
      { "field": "signal.type", "op": "since", "values": ["company.incorporated"], "days": 90 },
      { "field": "site.region", "op": "in", "values": ["london"] },
      { "bundle": "sdm" },
      { "field": "email.exists", "op": "in", "values": [true] }
    ]
  }
}

Marketing at director level and above, or sales directors other than interim ones, excluding sole traders, partnerships and dissolved companies, with any, all and not nested.

{
  "schema": "mydatahub.audience/1",
  "dataset": "uk_contacts",
  "where": {
    "all": [
      {
        "any": [
          {
            "all": [
              { "field": "contact.title", "op": "matches", "terms": ["marketing"] },
              { "field": "contact.job_level_rank", "op": "lte", "value": 6 }
            ]
          },
          {
            "all": [
              { "field": "contact.title", "op": "matches", "terms": ["sales director"] },
              { "not": { "field": "contact.title", "op": "matches", "terms": ["Sales Director (Interim)"], "mode": "exact" } }
            ]
          }
        ]
      },
      {
        "not": {
          "any": [
            { "field": "company.subscriber_class", "op": "in", "values": ["sole_trader", "ordinary_partnership"] },
            { "field": "company.status", "op": "in", "values": ["dissolved", "liquidation"] }
          ]
        }
      }
    ]
  }
}

Managers and above at engineering companies in the Midlands, one contact per company.

{
  "schema": "mydatahub.audience/1",
  "dataset": "uk_contacts",
  "dataset_version": "2026-W39",
  "where": {
    "all": [
      { "bundle": "manager_and_above" },
      { "field": "company.sic_2007", "op": "in", "values": ["25", "28", "29"] },
      { "field": "site.region", "op": "in", "values": ["east_midlands", "west_midlands"] },
      { "field": "company.turnover_band", "op": "gte", "value": "2.1M-5M" }
    ]
  },
  "dedupe": "one_per_company"
}

Build one from a brief

Send plain English to POST /v1/audiences/compile or call compile_audience. You get the audience, a one-line readback, a chip for each phrase with the words it came from, and any questions that need an answer, such as which Manchester. Nothing ambiguous is resolved silently.

{ "brief": "300 finance directors at law firms in Manchester" }

The chips are the truth and the count is the receipt. Run the compiled audience through POST /v1/counts to see its size.

Validate a definition

Validation errors are RFC 9457 problems. Each entry in errors has a JSON pointer such as /audience/where/all/0/field, a message and, where there is one, did_you_mean.

A field, operator, mode or code that is not recognised comes back with its closest valid values. See Errors.

For a local check, validate against the published JSON Schema, whose $id is https://mydatahub.emailmovers.com/schemas/audience/1.json. The schema holds which operator each field takes and the per-field value rules. One rule it cannot express is that a between range must be in order, which the server checks.

Next steps