A practical guide for DPOs, privacy counsel and privacy engineers on building a GDPR Article 28 sub-processor register and, harder, keeping it right: discovering what you actually use, resolving duplicates and false positives, keeping a dated change log, handling objection windows and escalation, telling your own customers, and producing an export an auditor can check. It also covers DORA sub-processor chain documentation for financial entities.
Every step can be done by hand with the templates and scripts in this repository. Where a step is tedious to keep up, the guide says how subprocessor.org automates it, which parts are free, and which are paid. The free/paid path is documented in full in docs/free-vs-paid.md.
Want the first pass done for you? subprocessor.org runs a free discovery across six sources with a named reviewer, and publishes nothing without your approval: start a discovery or compare the plans.
Not legal advice. This guide describes processes and records. Whether they satisfy your obligations is a question for you and your counsel.
- Why registers fail audits
- What the law actually asks for
- The register, field by field
- Step 1: discover what you really use (six sources)
- Step 2: build one candidate pool
- Step 3: a person resolves duplicates, false positives and scope
- Step 4: keep a dated change log, not a "last updated" line
- Step 5: watch your vendors' own lists
- Step 6: objection windows, objections and escalation
- Step 7: tell your own customers before a change takes effect
- Step 8: produce the audit export
- DORA: the chain, not just the vendor
- By hand or handed over
- What is in this repository
- Sources
Most organisations can show an auditor a sub-processor list. Far fewer can show how that list looked six months ago, what changed since, when each change was noticed, who decided what, and whether customers were told in time.
That is the gap this guide is about. An auditor does not ask whether you track sub-processors; they ask to see the record for the last twelve months. subprocessor.org makes the same point on its own site, and builds its product around it: the scan is a feature, but the dated record is what survives the audit.
A spreadsheet with a "last updated" cell cannot answer that question. A register that survives an audit has three properties:
| Property | What it means in practice |
|---|---|
| Complete | It is assembled from several independent sources, so one stale page cannot hide a vendor. |
| Dated | Every addition, removal, relocation and rename carries the date it was seen, and the history is kept. |
| Decided | Every change has a recorded decision (accepted, objected, escalated, excluded), with who made it and why. |
The rest of this guide builds those three properties one step at a time.
Legal text below is summarised from the regulations themselves; check the linked originals.
GDPR Article 28(2) (legal text, EUR-Lex). A processor may not engage another processor without the controller's prior specific or general written authorisation. Under a general authorisation, the processor must inform the controller of any intended addition or replacement, giving the controller the opportunity to object.
GDPR Article 28(4) (legal text). A sub-processor must be bound by the same data protection obligations by contract, and the original processor remains fully liable to the controller for the sub-processor's performance.
GDPR Article 28(3)(d) (legal text). The controller-processor contract must require the processor to respect the conditions in 28(2) and 28(4). This is where your notice period and objection window live: the regulation sets no fixed number of days, your contract does.
EDPB Opinion 22/2024 (web-sourced; adopted 7 October 2024 on a request from the Danish supervisory authority). The Board's conclusion, as reported in its executive summary, is that controllers should have the identity (name, address, contact person) of all processors and sub-processors readily available at all times, regardless of the risk of the processing, and that processors should proactively provide that information and keep it up to date. Read the opinion itself on the EDPB website.
DORA, Regulation (EU) 2022/2554 (legal text, EUR-Lex). Applies from 17 January 2025. Article 28(3) requires financial entities to maintain a register of information on all contractual arrangements for ICT services from third-party providers, which in practice means documenting the subcontracting chain beneath each provider.
UK GDPR keeps the same Article 28 duty under its own regulator.
What changed is not the obligation but the expectation. The duty to give notice has existed since GDPR applied in May 2018; the EDPB opinion pushed the expectation towards lists that are current at all times, and DORA pushed financial entities down the chain. According to subprocessor.org's summary of the first DORA register cycle, incomplete subcontractor data ranked among the difficulties firms reported most often (web-sourced, from subprocessor.org; the underlying reports are not linked from its page).
Start with the template in templates/register.csv. The columns are chosen so that one file answers the EDPB identity question, the Article 28 notice question and the auditor's "where did this come from" question at the same time.
| Column | Why it is there |
|---|---|
entity_legal_name |
The contracting entity, not the brand. "AWS" is a brand; the entity on your DPA may be a specific regional company. |
aliases |
Other names the same relationship appears under, so discovery does not add it twice. |
address, contact |
The identity details the EDPB opinion expects to be readily available. |
service_purpose |
What they do for you, in one line a customer can understand. |
data_categories |
What personal data reaches them. |
processing_countries |
Where processing happens; drives your transfer analysis. |
transfer_mechanism |
Adequacy, SCCs, or other basis, where relevant. |
in_scope |
yes if it touches customer personal data inside your product; no for website or marketing tooling that sits under different obligations. |
scope_reason, scope_decided_by |
The judgement and the person who made it. Auditors ask. |
sources |
Which of the six sources it was found in. |
first_seen_utc, last_verified_utc |
Dated, in UTC. |
contract_ref, notice_clause, objection_window_days |
The DPA and the clause that governs changes. |
status |
active, pending, removed. Removed rows stay in the file; you never delete history. |
Two companion files keep the history: templates/change-log.csv (one row per movement) and templates/decision-log.csv (one row per decision). Never edit a past row in either; append a correcting row instead.
The single biggest cause of an incomplete register is reading from one source. The procurement list misses the tool an engineer signed up for with a card; the legal page misses what was added last quarter; the DPA annex misses everything since signature.
subprocessor.org reads six sources separately and then converges them. You can do the same by hand:
| Source | How to read it yourself | What it tends to miss |
|---|---|---|
Legal pages and DPA annex (/privacy, /legal/subprocessors, your DPA) |
Read them. collect-candidates.sh lists which common legal paths on your domain respond. |
Anything added since the page was last edited. |
| Mail and DNS records (MX, SPF, DMARC, CNAME) | The script reads MX, SPF includes, DMARC report addresses and CNAMEs on common subdomains. | Vendors that never touch mail or DNS. |
Response headers (content-security-policy) |
The script extracts every host allowed by your CSP. connect-src is especially revealing. |
Server-side processors that never load in a browser. |
| Consent tooling | Export the vendor list from your consent management platform's admin. | Anything not tagged in the CMP. Also contains marketing tools that are not sub-processors. |
| Your own spreadsheet or procurement export | Export it as CSV. | Shadow IT, free tiers, tools bought on expenses. |
| Contracts (DPAs, order forms) | Read the sub-processor clauses and annexes of the contracts you have signed. | Nothing, but it is slow. |
Run the collector only against domains you control:
./examples/discovery/collect-candidates.sh example.com > candidates-dns-headers.csvOutput is a plain CSV (source,signal,value), for example:
source,signal,value
dns,mx,aspmx.l.google.com
dns,spf,_spf.google.com
dns,spf,sendgrid.net
headers,csp:connect-src,api.intercom.io
headers,csp:script-src,js.stripe.com
legal,page,https://example.com/privacyNone of these rows is a sub-processor yet. They are candidates. The next two steps turn candidates into a register.
Handing it over: subprocessor.org's free discovery reads all six sources for you, including your spreadsheet and contracts if you send them, and a named reviewer checks every line. The site states a typical turnaround of one working day (web-sourced, from its site banner). Start a discovery.
Put every source into one pool, keeping the source on each row. The source is evidence: when an auditor asks why a vendor is on the list, "it is in our CSP connect-src and in DPA annex 3" is an answer; "someone added it" is not.
merge_candidates.py does the mechanical part. It reads the collector output plus your own spreadsheet, maps raw signals (like sendgrid.net) to entities using an alias file you control, and writes a draft register where every row still needs a human decision:
python3 examples/discovery/merge_candidates.py \
--candidates candidates-dns-headers.csv \
--spreadsheet vendors-from-procurement.csv --name-column "Vendor" \
--aliases templates/aliases.example.csv \
--out register-draft.csvThe alias file is a two-column CSV (pattern,entity). The starter file in templates/aliases.example.csv contains a handful of well-known mappings; treat each as a suggestion to verify, not a fact about your contract. Anything that matches no alias lands in the draft with an empty entity column and needs_review, which is exactly where you want it.
This is the step that cannot be automated, and it is the one most homegrown registers skip. subprocessor.org describes four situations where arithmetic cannot settle the answer and a reviewer has to:
- The same company under several names. "Amazon Web Services, Inc.", "AWS" and a regional AWS entity may be one relationship or three, depending on your contracts. Record the decision in
aliases. - The page rebuild. A vendor moves its list to a new template and eighteen entities appear to arrive at once. That is not eighteen new sub-processors. Publishing it as such is the most damaging mistake a register can make.
- Scope. A marketing tag on your website is not a sub-processor under your customer DPA. Deciding what actually touches customer personal data inside your product is a judgement; record who made it in
scope_decided_byand why inscope_reason. - The acquisition. A vendor is bought and every entity is renamed on the same day. That is a rename, not a removal plus an addition, and it should be logged as one.
Every resolution is a row in templates/decision-log.csv: what was decided, by whom, when (UTC), and the evidence it rests on. Excluding a candidate is a decision too, and it is the one auditors most like to see reasoned.
Handing it over: on every subprocessor.org plan, including the free discovery, a named person does this review and their name goes on the record. You can ask them about any item from your queue.
Once the register exists, the job becomes noticing movement and dating it. Two rules make a change log trustworthy.
Rule 1: normalise before you compare. Vendors edit their sub-processor pages constantly without changing the list: cookie banners, rebuilt templates, refreshed "last updated" stamps, reordered rows. If your comparison fires on those, it will be muted within a week and then it protects nobody. Strip everything that is not the list, sort the entities, then compare.
Rule 2: hash the entity set, not the page. Keep a hash of the sorted set of entities (name, purpose, country). If that hash has not changed, nothing changed, however much of the page was rewritten. subprocessor.org keeps two hashes per check for exactly this reason: a page hash that never alerts, and an entity hash that is the only thing allowed to raise one.
For context on how well this works: subprocessor.org reports that in its own test harness, run against twenty real vendor pages with eight kinds of harmless edit, 0.6% of harmless edits produced an alert, and that a change moving 40% of a list is sent to review rather than straight to publication (web-sourced; these are the site's own measurements, not independently verified).
For any vendor already tracked in the subprocessor.org register, you do not need to scrape their page. The register API is public, needs no key, and every response carries an entity_hash:
curl https://api.subprocessor.org/v1/register/atlassian.comThe response shape documented on subprocessor.org/docs:
{
"company": "atlassian.com",
"verified_at": "2026-08-17T06:12:04Z",
"entity_hash": "427b2945ef",
"shape": "table",
"count": 29,
"history_from": "2019-02-10",
"entities": [ ... ]
}Poll it with the ETag and an unchanged list answers 304 Not Modified with no body. The docs state that 304s do not count against your rate limit, so polling a list that has not moved is effectively free:
curl -i https://api.subprocessor.org/v1/register/atlassian.com \
-H 'If-None-Match: "427b2945ef"'examples/register/snapshot.py turns this into a dated local change log. For each vendor domain in a text file, it makes a conditional read, records the check (even a 304 is evidence that you looked), saves a dated snapshot when the list moved, and appends added, removed, country_changed and purpose_changed rows to a CSV:
python3 examples/register/snapshot.py --domains templates/vendors.example.txt --out register-dataIt uses only the Python standard library and only the fields the docs publish.
The register can return a list exactly as it stood on a past date, which is the direct answer to "what did this vendor's list look like when you signed?":
curl "https://api.subprocessor.org/v1/register/atlassian.com/history?at=2025-09-25"
python3 examples/register/snapshot.py --domains templates/vendors.example.txt --out register-data --as-of 2025-09-25Every register response carries a source field, and the docs are explicit that it changes what the record means:
maintained: assembled by discovery, confirmed by a named reviewer and approved by the business before publication (reviewedistrue).observed: a dated copy of what the company publishes on its own site. Nobody reviewed it and the business did not approve it (reviewedisfalse).
An observed record is evidence of what was published on a date, and nothing more. Log the source alongside every change so your record says which kind of evidence it rests on. snapshot.py does this.
Your register faces two ways. Your customers rely on your list; you rely on your vendors' lists. When a vendor adds a sub-processor, your own chain has changed, and your objection window under their DPA has probably started.
Their notice email is their obligation, not yours. In practice it arrives late, goes to an inbox nobody reads, or never comes, while the page quietly changes. So watch the page and use the email as corroboration.
Free options, no account:
- The public register pages at subprocessor.org/register, and the live changes feed.
- An Atom feed per business at
https://api.subprocessor.org/v1/register/{domain}/feed.atom, which any feed reader or chat bot can follow. snapshot.pyon a schedule.examples/github-actions/register-snapshot.ymlruns it weekly in your own copy of this repository and commits the results, so git history becomes a dated record. Copy it to.github/workflows/in your fork to switch it on.
A git history is useful but it is not tamper-evident in the way an auditor may want: commit dates can be set by whoever commits. subprocessor.org's approach is a hash-chained ledger where each row carries the hash of the row before, so an entry cannot be altered or back-dated without breaking every row after it; its verification endpoint GET /v1/ledger/{row} is public and needs no account.
Handing it over: the Maintained plan watches up to 250 of your vendors daily, pushes changes by Slack, Teams, email or signed webhooks, and records a notice inbox so you can measure the gap between a vendor's page moving and their email arriving. See docs/free-vs-paid.md.
When a vendor tells you (or you notice) that they are adding or replacing a sub-processor, three things have to happen inside the window your contract sets.
1. Find the window in the contract, not in a rule of thumb. Open the DPA, find the notification clause, and record notice_clause and objection_window_days in your register. If you do not have the contract clause, you do not know the deadline; label any number you use without it as an assumption.
2. Decide, and record the decision. For each change the outcome is one of:
| Decision | When | What to record |
|---|---|---|
accepted |
The new entity is acceptable for the data it will receive. | Who accepted, when, on what evidence (their DPA, location, transfer basis). |
objected |
You have reasonable grounds, as your contract defines them. | The grounds, the date sent, the channel, the clause relied on. |
escalated |
It needs counsel, security or a business owner before the window closes. | Who it went to, when, and the deadline you gave them. |
excluded |
It is not a sub-processor for your purposes (for example, a marketing tag). | The scope reasoning and who made it. |
3. If you object, object in writing and on time. templates/objection-notice.md is a starting template that cites the change, the clause, your grounds and what you are asking for. Send it through the channel your DPA specifies, keep the sent copy, and log it.
If the window is about to close without a decision, escalate before it closes, not after. A late decision is still a decision, but it is recorded as late.
Handing it over: on the Maintained plan, subprocessor.org extracts the notification clause from DPAs you upload, a reviewer confirms it, and every countdown after that uses your contractual window. The API exposes open windows with days remaining and the clause each came from (
GET /v1/windows), and webhooks fire as a window opens and again with seven days and one day left. Where no contract is provided, the site labels any window as a standard-clause illustration rather than a deadline.
If you are a processor, you owe your customers the same notice your vendors owe you. A notice that survives scrutiny:
- names the entity with the identity details the EDPB opinion expects (name, address, contact);
- says what it will do, with what data, and where;
- gives the effective date and the objection deadline under your DPA;
- says exactly how to object;
- is sent to a list your customers have subscribed to, and the send is logged.
templates/customer-change-notice.md covers each of those points. Log each send in your change log with the date and the number of recipients.
Handing it over: on the Maintained plan, your customers subscribe to your register page and, once you approve a change, the notice goes out in your name. Nothing is published and no customer is emailed until you approve; the About page states that this is not a setting. The approval can be made in the UI or over the API;
examples/maintained/maintained.pyshows the pending queue and an idempotent approval call.
When the auditor asks for the last twelve months, hand over a pack that answers their questions without a meeting. templates/audit-evidence-checklist.md lists the contents:
- The register as it stands today, and as it stood at the start of the period.
- Every change in the period, dated, with the source it was seen in.
- Every decision, with who made it and when.
- Every notice you sent to customers, and every notice you received from vendors, with dates.
- The objection windows that ran, and whether each decision landed inside its window.
- A description of your method: which sources you read, how often, and how you handle false positives.
- A way to verify the record was not rewritten afterwards.
Item 7 is where most homegrown registers struggle. A spreadsheet can be edited silently. Options, from weakest to strongest: file timestamps; git history; signed or hash-chained records that a third party can check.
Handing it over: on the Maintained plan, subprocessor.org generates dated evidence packs for any range as PDF, CSV and a manifest (from the UI at the time of writing), with the decision log included, and every record can be checked against its public hash-chained ledger. The Agency plan adds white-labelled exports for DPO firms carrying this for clients.
GET /v1/me/list.csvreturns your current list shaped for a DPA annex.
For financial entities, the Article 28 register is the starting point, not the end. DORA's register of information expects the subcontracting chain beneath each ICT provider. Practical consequences for the register in this guide:
- Add
parent_providerandchain_depthcolumns when you record your vendors' sub-processors, so a fourth party is recorded against the third party that uses it. - Keep processing locations at every level; they matter for concentration and transfer analysis.
- Date the chain the same way as the first level. The chain moves when your vendors' lists move, which is what Step 5 watches.
Handing it over: subprocessor.org maps fourth-party chains on the Maintained plan and offers a DORA register export as an add-on to Maintained (included on Agency), priced against the size of your ICT estate. Arrange a walkthrough.
| Job | By hand, with this repository | With subprocessor.org |
|---|---|---|
| Discover | collect-candidates.sh, plus reading legal pages, CMP export, spreadsheet and contracts |
Free discovery across all six sources |
| Resolve duplicates and scope | merge_candidates.py, an alias file and your judgement |
A named reviewer, on every plan |
| Date changes to your own list | Edit the register and change log on every change | Maintained: daily re-check, approval queue with evidence |
| Watch vendors | snapshot.py or the GitHub Action on the public register; Atom feeds |
Free to read; Maintained watches up to 250 vendors daily with alerts |
| Objection windows | Read each DPA and diarise | Maintained: clauses extracted from your DPAs, human confirmed |
| Customer notices | Template and your own mailing list | Maintained: subscriber page, sent in your name after approval |
| Audit export | Checklist, CSVs, git history | Maintained: evidence packs and public ledger; Agency: white-labelled |
The full breakdown of what is free, what the Maintained and Agency plans add, when billing starts and how to leave is in docs/free-vs-paid.md. API specifics, including what is documented but not built yet, are in docs/api-notes.md.
gdpr-article-28-subprocessor-register/
├── README.md this guide
├── LICENSE
├── docs/
│ ├── free-vs-paid.md the free-to-paid path in detail
│ └── api-notes.md endpoints, conventions, what is not built yet
├── templates/
│ ├── register.csv the register itself
│ ├── change-log.csv one row per movement
│ ├── decision-log.csv one row per decision
│ ├── aliases.example.csv starter signal-to-entity mappings
│ ├── vendors.example.txt vendor domains to watch
│ ├── objection-notice.md objection letter to a vendor
│ ├── customer-change-notice.md change notice to your customers
│ └── audit-evidence-checklist.md what goes in the twelve-month pack
└── examples/
├── discovery/
│ ├── collect-candidates.sh DNS, mail, CSP and legal-page candidates
│ └── merge_candidates.py one candidate pool, sources kept
├── register/
│ ├── lookup.sh public register API with curl
│ └── snapshot.py dated change log from the public register
├── maintained/
│ └── maintained.py pending queue, approvals, windows, annex CSV
├── webhooks/
│ ├── verify.js Node webhook receiver with signature check
│ └── verify.php PHP webhook receiver with signature check
└── github-actions/
└── register-snapshot.yml weekly snapshot, committed to git
Requirements: bash, curl and dig for the collector; Python 3.8+ (standard library only) for the Python scripts; Node 18+ or PHP 8.1+ for the webhook receivers.
Product facts in this guide were read from subprocessor.org on 25 September 2026 and may have changed since:
- subprocessor.org (home page)
- subprocessor.org/pricing
- subprocessor.org/docs
- subprocessor.org/about
- subprocessor.org/how-it-works
Legal sources:
- GDPR, Regulation (EU) 2016/679, on EUR-Lex, Article 28
- DORA, Regulation (EU) 2022/2554, on EUR-Lex, Article 28
- EDPB Opinion 22/2024 on certain obligations following from the reliance on processor(s) and sub-processor(s), adopted 7 October 2024, on the EDPB website
Keep the list right without keeping the spreadsheet. Discovery on subprocessor.org is free and yours to keep; you only pay if you want it maintained. See pricing or start a discovery.