# Changelog of Website Contact Scraper – Emails, Phones & Socials (`emastra/website-contact-scraper`) Actor

- **URL**: https://apify.com/emastra/website-contact-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/emastra/website-contact-scraper.md

## Changelog

All notable changes to Website Contact Scraper (formerly Domain Contact Scraper). The output contract stays at `schemaVersion: 1`: no field was renamed or removed. Several releases did change how contact **values** are formatted; those changes are listed under **Output format** so integrations that match or deduplicate on exact strings can adapt.

### 1.2 — 2026-11-03

Data-quality release: fewer false positives, cleaner social links, and the same pages visited on every run.

#### Output format

- **Phones**
  - On websites without country context (for example a `.com` site with no `defaultPhoneCountry`), a number found in plain page text is kept only when a phone label sits next to it ("Tel:", "Phone", ☎, …). Numbers from `tel:` links and structured data are unaffected. This removes product codes, statistics and order numbers that were returned as phones.
  - North American numbers written with the country code but without `+` (`1-800-778-7879`) are now returned in E.164 (`+18007787879`).
  - Dates, year/number references (`2016/679`), US ZIP+4 codes, placeholder numbers (`9876543210`) and company or tax IDs (VAT/P.IVA, USt-IdNr, business register numbers, US EIN and tax IDs, UK company numbers) are no longer returned as phones, even on websites with a country.
- **Social profiles**
  - Handles are lowercased (`https://twitter.com/Acme` → `https://twitter.com/acme`), so the same account linked with different capitalization appears once. YouTube channel IDs and Telegram invite codes keep their case.
  - Links to a post or sub-page are reduced to the account (`linkedin.com/company/acme/about` → `linkedin.com/company/acme`, a tweet → the account).
  - Country, mobile and `www` subdomains are folded into the main domain (`es.linkedin.com` → `linkedin.com`), and `telegram.me` into `t.me`.
  - Videos and embeds (`youtube.com/watch`, `/embed`, `/playlist`, Instagram posts and reels), share buttons, login and legal pages, and tracking URLs are no longer returned.
  - `facebook.com/profile.php?id=…` keeps its `id` (it was previously returned as the unusable `facebook.com/profile.php`).
- **Emails**
  - Escape and markdown leftovers are removed (`u003ehelp@acme.com`, `**help@acme.com` → `help@acme.com`).
  - Placeholder addresses are rejected (`yourname@…`, `john.doe@…`, and `email@`, `name@`, `user@` on free-mail domains such as Gmail).
  - The domain must end in a real public suffix. Text glued onto an address is trimmed when possible (`info@acme.it.importante` → `info@acme.it`) and the address is dropped otherwise.

#### Crawling

- Pages within the page budget are chosen by a fixed priority, not by which pages load fastest: contact, legal notice (Impressum, mentions légales, note legali), privacy policy, support, about, then other legal pages. Repeated runs on the same website visit the same pages, and the contact page is no longer skipped when it loads slowly.
- When a page fails, the next candidate page takes its place.
- More tracking parameters are ignored when deciding whether two links point to the same page (ad click IDs such as `msclkid`, and campaign parameters such as `demo_source`), so one page is no longer visited twice through differently tagged links. `startUrl` drops them too.
- `crawl.terminationReason: "page_limit_reached"` (legacy `stoppedReason: "budget"`) still means the page budget was used up while more candidate pages were waiting.

#### Removed

- The **Checkpoint interval** input (`checkpointIntervalSec`). Its snapshots were never read back, so it gave no crash resilience, and the snapshot key was rejected by the platform: setting it made pages fail once the first interval elapsed. Saved inputs and tasks that still contain the field keep working; it is ignored.

#### Upgrade notes

- If you deduplicate or join on social URLs, normalize stored values the same way (lowercase the handle) or re-run affected websites.
- Expect fewer phones on websites without country context. Set `defaultPhoneCountry` when your list is from a single country: numbers then validate against that country and need no label.

### 1.1 — 2026-08-18

#### Added

- `status` (`completed`, `partial`, `failed`) and `hasContacts` on every record.
- Crawl diagnostics: `pagesAttempted`, `pagesSucceeded`, `pagesFailed`, `startedAt`, `finishedAt`, `durationMs`, `terminationReason`, `startPageFinalUrl`, `startPageHttpStatusCode` and a sanitized `error` (`code`, `message`, `retryable`, `url`).
- `defaultPhoneCountry` input: fallback country for local phone numbers.
- Results are written as soon as each website finishes, instead of at the end of the run.

#### Output format

- **Phones** are validated with libphonenumber and returned in E.164 (`+15551234567`) when a country is known from the website or `defaultPhoneCountry`. Numbers without a resolvable country stay in local form with normalized spacing. Previously numbers were returned as written on the page (`+1 555 123 4567`).
- **Social profiles** are canonicalized: `https`, no `www.`, no query parameters on social hosts, and alias hosts folded (`x.com` and `mobile.twitter.com` → `twitter.com`, `m.facebook.com` → `facebook.com`).
- **Emails** are validated more strictly (code and package strings, asset filenames and placeholder domains are rejected), and addresses from `mailto:` links with query strings or several recipients are recovered.
- **Dataset order** follows completion order instead of input order. Match records on `startUrl`.

### 1.0

Initial release: one consolidated record per website with emails, phones, WhatsApp numbers and social profiles, priority-first crawling and configurable crawl scope.
