EmailmoversMyDataHub docs
Browse the docs

Guide

Compliance receipts

Every order issues a signed receipt that proves what was delivered and how it was screened. Verify its ES256 signature with the published key, or with one request and no account.

Updated 4 October 2026View as Markdown

Every order issues a compliance receipt: a compact JWS signed with ES256 that counts the records delivered, the suppressions removed, the subscriber classes, the TPS and CTPS screens and the Article 14 status. Anyone can verify it with the published key, or with POST /v1/compliance/receipts/verify and no account. One agent can therefore prove to another that a list is licensed and screened.

Get the receipt

GET /v1/orders/{id}/receipt and get_compliance_receipt return it once the order is ready. A PDF twin is at mydatahub.emailmovers.com/receipts/{id}. The response has the compact jws, its alg and kid, the jwks_url, and the decoded claims.

{
  "id": "rcpt_01JB4NA2Q5W8ZAK8Q2V7XN3M9R",
  "order_id": "ord_01JB4N9S3W2X6Y7Z8A1B2C3D4E",
  "alg": "ES256",
  "kid": "rcpt-2026-09",
  "jwks_url": "https://mydatahub.emailmovers.com/.well-known/jwks.json",
  "pdf_url": "https://mydatahub.emailmovers.com/receipts/rcpt_01JB4NA2Q5W8ZAK8Q2V7XN3M9R",
  "claims": {
    "receipt_id": "rcpt_01JB4NA2Q5W8ZAK8Q2V7XN3M9R",
    "order_id": "ord_01JB4N9S3W2X6Y7Z8A1B2C3D4E",
    "issued_at": "2026-09-26T10:16:04Z",
    "dataset_version": "2026-W39",
    "records_delivered": 5000,
    "removed_by_suppression": 41
  }
}

What the claims say

ClaimMeaning
receipt_id, order_id, issued_atWhich receipt, for which order, and when
dataset_versionThe immutable build the records came from
records_delivered, removed_by_suppressionWhat you received and what was left out
counts.subscriber_classRecords by ltd, plc, llp, scottish_partnership, public_body, charity_incorporated, sole_trader, ordinary_partnership and unknown
counts.tps_ctps_statusRecords clear, tps_registered, ctps_registered, tps_or_ctps_registered or not_screened
counts.article14_statusRecords notified, publicly_notified_disproportionate_effort or pending_within_window
lawful_basis, lia_refUK GDPR Article 6(1)(f) legitimate interests, and the published assessment
licenceThe licence block: type, reuse_until, channels, warranty and terms version
residency_statement_version, sub_processor_list_versionWhich versions of those published statements applied
verified_within_daysThe share of delivered emails verified within 30, 60 and 90 days
content_sha256SHA-256 of the delivered file, in lower case hex

Verify with one request

POST /v1/compliance/receipts/verify takes the JWS and needs no account.

curl -X POST "https://mydatahub.emailmovers.com/v1/compliance/receipts/verify" \
  -H "Content-Type: application/json" \
  --data '{ "jws": "eyJhbGciOiJFUzI1NiIs..." }'
{
  "valid": true,
  "order_exists": true,
  "licence_in_force_until": "2027-09-26",
  "revoked": false,
  "receipt_id": "rcpt_01JB4NA2Q5W8ZAK8Q2V7XN3M9R",
  "checked_at": "2026-10-01T09:00:00Z"
}

Read the three answers separately.

  • valid is the signature alone. It says the receipt was signed by a published MyDataHub key, with the receipt type in the header.
  • order_exists says whether the order is on record.
  • licence_in_force_until is a date only when the order is on record, live, not revoked and not ended. It is null otherwise, so a signature alone never reads as a licence.

A JWS that does not verify returns valid false and nothing else. Its claims are never read. This operation is REST only, because the 22 MCP tools are fixed.

Verify offline

You can check a receipt with no call to us. Fetch the key set, verify the signature, and check the typ header, because a quote and a confirmation token are signed by the same key.

A receipt's protected header has typ set to mydatahub.receipt+jws. Receipts signed before a key rotation still verify, because the key set lists every retired key.

// verify-receipt.ts
import { compactVerify, createRemoteJWKSet } from "jose";

const jwks = createRemoteJWKSet(new URL("https://mydatahub.emailmovers.com/.well-known/jwks.json"));

export async function verifyReceipt(jws: string): Promise<Record<string, unknown>> {
  const { payload, protectedHeader } = await compactVerify(jws, jwks, { algorithms: ["ES256"] });
  if (protectedHeader.typ !== "mydatahub.receipt+jws") throw new Error("This JWS is not a receipt.");
  return JSON.parse(new TextDecoder().decode(payload)) as Record<string, unknown>;
}

This needs the jose package. Then check the file you were given against the receipt.

// check-file.ts
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";

export async function matchesReceipt(path: string, claims: { content_sha256: string }): Promise<boolean> {
  const digest = createHash("sha256").update(await readFile(path)).digest("hex");
  return digest === claims.content_sha256;
}

The hash records the file on the day of delivery. A copy downloaded later no longer matches once a record in it was objected to, which blanks that person's details, or once a number's clear TPS and CTPS screen is more than 28 days old, which makes it read not_screened. See Suppressions and objections.

The compliance object on every record

Every preview row, order record, lookup and enrichment carries a compliance object, repeated as CSV columns and as the MyDataHub property group on a CRM push.

MemberValues
lawful_basislegitimate_interests_art6_1_f
subscriber_classltd, plc, llp, scottish_partnership, public_body, charity_incorporated, sole_trader, ordinary_partnership or unknown
email_marketing_basispecr_corporate_subscriber or consent_required
article14_statusnotified, publicly_notified_disproportionate_effort or pending_within_window, with article14_notified_at and notice_version
tps_ctps_statusclear, tps_registered, ctps_registered, tps_or_ctps_registered or not_screened: the result of the screen on tps_ctps_screened_at, valid until tps_ctps_valid_until. In a sample, an order's records (each time they are fetched, downloaded or pushed) and an enrichment, a clear number reads not_screened once that screen is more than 28 days before the day the record is served
suppression_statusnone, objected_to_emailmovers, objected_to_licensee, deceased or left_company
provenanceThe source of the company, contact and email, with dates, the verification method and the provider location

Companies House company data is under the Open Government Licence v3.0 and carries its attribution. Officer names are personal data outside that licence, so they carry an Article 14 status instead.

The buyer kit

Each order returns buyer_kit with four signed URLs.

  • A draft legitimate interests assessment.
  • First-email wording that satisfies Article 14 at first communication.
  • An Article 30 stub for your record of processing.
  • A due diligence file with the receipt, the validation report and the order's audit trail.

The documents are made in the app, and the links appear once they exist.

How it answers the ICO

The ICO's guidance for buyers of broker data says accepting a broker's assurances is not enough, so the evidence is fields and not badges.

ICO questionWhere to look
Who compiled the data?provenance.contact.source, and the data owner statement on the trust page
Where was it obtained from?The source per field in provenance, and the Companies House attribution
What privacy information was used?article14_status, article14_notified_at and notice_version
When was it compiled and how old is it?compiled_at, last_verified_at, record_age_days and dataset_version
How was it collected?provenance.contact.method and the verification provider and location
What consent records exist?None claimed. The basis is legitimate interests, with email_marketing_basis per record
Was it screened against opt-out and suppression lists?tps_ctps_status and its screen date, suppression_status, suppressed_removed per count and the receipt counts

The trust page is also data: GET /v1/trust, GET /v1/compliance/lia, the mydatahub://compliance resource and https://mydatahub.emailmovers.com/trust.json.

Next steps