Vendor privacy contact sheet: each supplier's DPO, privacy or grievance inbox from its own website, with the page it was found on and a re-check date
What you get: one Google Sheet with one row per vendor that handles personal data for your business (payroll, email marketing, CRM, hosting, an AI chatbot, a booking tool). Each row shows the vendor's best privacy contact taken from its own website (a DPO inbox first, then a privacy or data-request inbox, then a grievance inbox, then legal or compliance, then a general inbox), the page it was found on, its security inbox if listed, a Status column (OK / Weak / Missing / Skipped) and a Re-check on date. You use it the day a customer asks what a vendor holds about them, or a vendor tells you about a breach and you need to write back to the right place. Formulas are below. The example rows are made up.
The finished sheet
privacy tab (example rows are made up)
| Domain | Status | Best privacy contact | Type | Found on | Security inbox | Last checked | Re-check on | Next step |
|---|---|---|---|---|---|---|---|---|
| payrollco.example | ok | dpo@payrollco.example | DPO | payrollco.example/privacy | security@payrollco.example | 27 Oct 2026 | 27 Apr 2027 | OK: DPO inbox on file |
| mailblast.example | ok | privacy@mailblast.example | Privacy | mailblast.example/legal/privacy | 27 Oct 2026 | 27 Apr 2027 | OK: privacy inbox on file | |
| chatbotly.example | ok | legal@chatbotly.example | Legal | chatbotly.example/terms | 27 Oct 2026 | 27 Apr 2027 | Weak: legal inbox only, read its privacy policy | |
| smallhost.example | ok | info@smallhost.example | General | smallhost.example/contact | 27 Oct 2026 | 27 Apr 2027 | Weak: general inbox only, ask for a privacy contact | |
| bigcloud.example | blocked-by-terms | Skipped: copy the contact from its privacy policy by hand |
The six-month re-check is our working rule, not a legal deadline. Change it in one cell if you prefer every three or twelve months.
Why keep vendor privacy contacts in one place
- A vendor has a breach. Under GDPR Article 33(2), a processor must notify the controller (you) without undue delay after becoming aware of a personal data breach. You then have your own 72-hour clock under Article 33(1). Knowing who to write back to at the vendor saves hours. Our 72-hour breach sheet covers your side.
- A customer asks for their data. Article 28(3)(e) says the processor contract must require the processor to help you answer data subject requests. You still need to reach the right team.
- The vendor has to publish it anyway. Under Article 37(7), a controller or processor that has a data protection officer must publish the DPO's contact details, and Article 13(1)(b) puts the DPO's contact details in the privacy notice where there is one. So the contact you need is usually already on the vendor's own website.
- India: Rule 9 of the Digital Personal Data Protection Rules, 2025 requires every Data Fiduciary to prominently publish on its website or app the business contact information of its DPO, if it has one, or of a person who can answer questions about processing. Rule 9 is in the group that comes into force eighteen months after the Rules were published on 13 November 2025, which works out to 13 May 2027. Until then many Indian vendors will list a grievance officer or a general inbox instead.
The sheet sits next to your record of processing. If you are building that too, see the Article 30 records of processing sheet and the Article 28 processor agreement check sheet.
Step 1: list your vendors in a tab named vendors
| Column | What to put |
|---|---|
| A Vendor | Name, for example "PayrollCo" |
| B Website | The vendor's homepage, for example https://www.payrollco.example |
| C What it does for you | Payroll, newsletter, CRM, website hosting, AI chatbot, appointment booking... |
| D Personal data it gets | For example "staff names, bank details" or "customer emails, chat transcripts" |
| E Role | Processor (acts on your instructions), Separate controller or Not sure |
| F Country | Where the vendor is based |
The quickest way to find vendors is your last 12 months of card and bank statements plus the "connected apps" page of your email and website tools.
Step 2: run the contact scraper on the vendor websites
Open the website contact scraper on the Apify Store and paste column B into urls (one per line). What it does, as written in the Actor's input schema and code on 27 October 2026:
- Reads the homepage, then the most contact-like pages first. Contact and imprint pages rank highest; links with
privacy,legal,aboutorsupportin them come next.maxPagesPerSiteis 4 by default. For this job set it to 8 so the privacy policy page is more likely to be read. - Returns only shared role inboxes.
dpo@,privacy@,dsar@,dsarequests@,grievance@,nodal@,legal@,compliance@andsecurity@are all on its role list.dataprotection@andgdpr@are not on the list, so with personal emails off they are dropped and only counted inpersonalEmailsDropped. If that count is above 0, open the privacy policy and check. - Records the page each inbox was found on (
emails/N/foundOn) and whether the domain can receive mail (emails/N/mxValid). - Follows robots.txt and reads each site's terms. Many large software companies' terms forbid scraping, so expect some rows to come back as
blocked-by-terms,blocked-by-tos-listorblocked-by-robots. Those are not charged; you copy their contact by hand.
When the run finishes, export the dataset as CSV, then in your sheet use File → Import → Upload, "Insert new sheet", and rename it scrape. Nested fields become headers like emails/0/email and emails/0/foundOn, so the formulas below find columns by header name, not by position.
Free route
For ten vendors, open each privacy policy, search the page for "@", and type what you find into a scrape tab with the headers domain, status (type ok), emails/0/email, emails/0/foundOn, emails/1/email, emails/1/foundOn and so on. The formulas work the same.
Step 3: build the privacy tab
Create a tab named privacy, put these headers in row 1 and these formulas in row 2, and fill down as many rows as scrape has.
| Col | Header | Formula (row 2) |
|---|---|---|
| A | Domain | =IFERROR(INDEX(scrape!$A2:$ZZ2, MATCH("domain", scrape!$A$1:$ZZ$1, 0)), "") |
| B | Status | =IFERROR(INDEX(scrape!$A2:$ZZ2, MATCH("status", scrape!$A$1:$ZZ$1, 0)), "") |
| C | All inboxes | =IFERROR(TEXTJOIN(", ", TRUE, FILTER(scrape!$A2:$ZZ2, REGEXMATCH(scrape!$A$1:$ZZ$1, "^emails/\d+/email$"))), "") |
| D | DPO inbox | =IFERROR(REGEXEXTRACT(", "&C2, ", (dpo(?:[.\-_][a-z0-9]+)?@[^,]+)"), "") |
| E | Privacy inbox | =IFERROR(REGEXEXTRACT(", "&C2, ", ((?:privacy|dsar|dsarequests)(?:[.\-_][a-z0-9]+)?@[^,]+)"), "") |
| F | Grievance inbox | =IFERROR(REGEXEXTRACT(", "&C2, ", ((?:grievance|grievances|nodal)(?:[.\-_][a-z0-9]+)?@[^,]+)"), "") |
| G | Legal inbox | =IFERROR(REGEXEXTRACT(", "&C2, ", ((?:legal|compliance)(?:[.\-_][a-z0-9]+)?@[^,]+)"), "") |
| H | General inbox | =IFERROR(REGEXEXTRACT(", "&C2, ", ((?:support|help|info|contact|hello)(?:[.\-_][a-z0-9]+)?@[^,]+)"), "") |
| I | Best privacy contact | =IF(D2<>"", D2, IF(E2<>"", E2, IF(F2<>"", F2, IF(G2<>"", G2, H2)))) |
| J | Type | =IFS(D2<>"", "DPO", E2<>"", "Privacy", F2<>"", "Grievance", G2<>"", "Legal", H2<>"", "General", TRUE, "") |
| K | Found on | =IFERROR(INDEX(scrape!$A2:$ZZ2, MATCH(SUBSTITUTE(INDEX(scrape!$A$1:$ZZ$1, MATCH(I2, scrape!$A2:$ZZ2, 0)), "/email", "/foundOn"), scrape!$A$1:$ZZ$1, 0)), "") |
| L | Security inbox | =IFERROR(REGEXEXTRACT(", "&C2, ", (security(?:[.\-_][a-z0-9]+)?@[^,]+)"), "") |
| M | Named inboxes dropped | =IFERROR(INDEX(scrape!$A2:$ZZ2, MATCH("personalEmailsDropped", scrape!$A$1:$ZZ$1, 0)), 0) |
| N | Last checked | Type the date you confirmed the contact (from the run date or by opening the page) |
| O | Re-check on | =IF(N2="", "", EDATE(N2, 6)) |
| P | Next step | =IFS(B2<>"ok", "Skipped ("&B2&"): copy the contact from its privacy policy by hand", AND(O2<>"", TODAY()>=O2), "Re-check now", J2="DPO", "OK: DPO inbox on file", J2="Privacy", "OK: privacy inbox on file", J2="Grievance", "OK: grievance inbox on file", AND(J2="Legal", M2>0), "Weak: legal inbox only, and a named or dataprotection@ inbox was dropped: read its privacy policy", J2="Legal", "Weak: legal inbox only, read its privacy policy", J2="General", "Weak: general inbox only, ask for a privacy contact", TRUE, "Missing: ask your account manager in writing") |
Notes on the formulas:
- Column K finds the header of the best inbox (for example
emails/3/email), swaps it foremails/3/foundOnand returns that page, so you can show where the contact came from. - Join the two tabs with the vendor name if you like: in
vendors,=IFERROR(INDEX(privacy!$I:$I, MATCH("*"®EXEXTRACT(B2, "//(?:www\.)?([^/]+)")&"*", privacy!$A:$A, 0)), "")pulls the best contact next to each vendor. - If
emails/N/mxValidisFALSEfor the inbox you picked, that domain has no mail server. Use the vendor's support form or account manager instead.
Step 4: fix the Weak and Missing rows
- Open the vendor's privacy policy and search for "data protection officer", "DPO", "grievance officer" or "privacy". Copy the inbox into the
scrapetab by hand. - Look for a privacy request form. Many larger vendors use a web form instead of an inbox; paste the form link into column I and type
Formin column J. - Still nothing: write to your account manager or support and ask, in writing, which address to use for data protection requests and breach notices. Put the answer in the sheet with today's date.
- Processors you rely on heavily (payroll, CRM, AI tools that see customer data): check the signed data processing agreement too. It often names a notice address that is different from the website.
Step 5: use it
Add a tab named plan. In A1: =QUERY(privacy!A2:P, "select P, count(A) where A is not null group by P order by count(A) desc label P 'Next step', count(A) 'Vendors'", 0). Work the Skipped, Missing and Weak rows first. Put a calendar reminder on the earliest Re-check on date.
Keep the sheet with the people who answer customer data requests. It holds only shared company inboxes, not anyone's personal address.
Partner programme opening soon
Partner programme opening soon: join the free waitlist. Planned terms are 50% of each referred sale (75% once you have 5+ paying customers), single level, paid through Gumroad only from revenue received.
Sources
- GDPR Article 13(1)(b), Article 28(3)(e), Article 30, Article 33(1) and (2), Article 37(7): gdpr-info.eu (read 27 October 2026).
- Digital Personal Data Protection Rules, 2025 (G.S.R. 846(E), 13 November 2025), rule 1(4) commencement and rule 9 contact information: MeitY.
- Contact scraper inputs and outputs (
urls,maxPagesPerSitedefault 4,includePersonalEmailsoff by default, role inbox list incl. dpo, privacy, dsar, grievance, nodal, legal, compliance, security;foundOn,mxValid,personalEmailsDropped, statusesblocked-by-terms,blocked-by-tos-list,blocked-by-robots): Apify Store listing and the Actor's input schema and code. - Google Sheets INDEX/MATCH, FILTER, REGEXEXTRACT, EDATE and QUERY functions: Google Docs Editors Help.
Related
- GDPR Article 30 records of processing sheet
- GDPR Article 28 processor agreement check sheet
- Personal data breach 72-hour sheet
- Supplier bank details change call-back sheet
- Clean a contact CSV into a call sheet
Unadhi Guides are free, independent explainers. We correct errors as soon as we find them: write to mininglife7@gmail.com.