Denmark’s CPR breach: Securing third-party access to your API
About 14 million lookups pulled 8.8 million records from Denmark's civil register through one company's lawful access, and every one of them authenticated. How to model third-party access in your own API so population, volume and purpose get checked too.
The Denmark CPR breach is a September 2026 incident in which about 14 million lookups ran against Denmark's Central Person Register through one private company's lawful access, returning names, addresses and CPR numbers for about 8.8 million people. No system vulnerability was exploited: the login was valid, and the queries stayed inside the data private companies are allowed to see. The register found out when it sent the company its invoice.
If you ship a B2B API that lets other companies look up data about your users, this is the failure to study. Every request authenticated. The partner was entitled to every field it got back. What it wasn't entitled to was the volume, or the people: the CPR office said the number of lookups "far exceeds the company's customer base." Nothing in the request path was checking either one.
What happened in the Denmark CPR breach?
Denmark's CPR is the national civil register. It holds about 11 million records, covering living residents, people who have died and people who moved abroad, against a population of about 6 million. The 10-digit CPR number it assigns is used for taxes, healthcare and banking. The 8.8 million records reached are about 80% of the register; Politiken reported the split as roughly 5.5 million living people and 3.3 million who have died. People registered with name and address protection were not included.
Private companies get in under section 38 of the Danish CPR Act. A company with a legitimate interest may receive information about a larger but limited group of people that it has identified individually in advance (by CPR number, by date of birth and name, or by name and address), and it must separately be entitled to that data under the GDPR and the Danish Data Protection Act.
Read that requirement again, because it is the whole story. The legal grant was never "query the register." It was "query this population, which you identified in advance, for this reason."

Access through the company began on September 10, according to documents reported by TV 2, and the lookups ran for about ten days, likely ending around September 20. The CPR administration became aware of irregular activity on the evening of Friday, October 2, established the scale over the weekend, notified the Danish Data Protection Agency (Datatilsynet) on Sunday, October 4, and disclosed the breach on Monday, October 5. It cut off the company's access and the police opened an investigation. Datatilsynet says it is examining what happened, how it was possible, and who is the data controller.
How did the attacker get in?
The official account says only that unauthorized parties misused a company's lawful access, and it still says no more than that. On October 8, an anonymous person told Politiken they were behind it and had logged in with a leaked password belonging to a former employee of the company. Politiken reported that at least three of the company's accounts, an administrator account among them, used the password 123456. The company, Pays ApS, a small IT firm in Odense, has confirmed it "has been subjected to an attack." Minister Christina Egelund has said security around the CPR system has not been good enough, and that access by the same method has been shut off. As of October 13, no arrest had been reported.
If the claim holds, it doesn't change the lesson for API builders. It sharpens it. You can't see a partner's password policy, their offboarding, or whether a former employee's login still works. From your side, a partner that goes rogue and a partner whose admin account was taken over look identical: a valid credential asking for fields it's allowed to ask for. Whatever you build has to catch both.
Why did billing catch it before monitoring did?
Companies pay for every CPR lookup, and that pricing model is what surfaced the incident. "I can confirm that it is during invoicing on Friday evening that a very, very large amount is billed," Mikkel Leihardt, a department head at the ministry, told reporters.
The lookups stopped around September 20. The signal arrived on October 2, from billing.
Billing worked as a detector for a boring reason: it was the only system in the path that counted volume per partner and had a person whose job was to look at the total. Every other layer judged requests one at a time, and one at a time, each request was fine.
Most API platforms share this blind spot. Rate limits exist to protect infrastructure, so they get tuned to what the database can survive, not to what a partner's stated business justifies. 14 million lookups over ten days averages out to about 16 requests per second: trivial traffic for any serious system, and nothing like an honest description of checking a customer list. An infrastructure limit would never fire, because as an infrastructure question nothing was wrong.
The hit rate was a second signal. About 8.8 million of 14 million lookups returned a record, roughly 63%. A partner checking customers it identified in advance should match almost every time, because it already holds their details. A third of lookups coming back empty is what enumeration looks like.
Why isn't an API key with scopes enough for partner access?
Think about the identity tiers you actually build. End users get roles and permissions. Admins get elevated scopes. Internal services get machine credentials. Then there's the integration partner: another company, with a contract, a declared purpose, and a defined population it may ask about. It usually gets an API key with a scope list, exactly like a service account.
Scopes answer one question: which endpoints may this caller hit? A partner grant has to answer more.
Authentication answers none of the last four. Denmark's failure was population and volume, and both live in a tier most API designs skip.

How do you secure third-party access to an API?
The unit of authorization is a grant: a partner, a declared purpose, a population and a quota, not an API key. Here's the shape behind a partner-facing read API.
This only works if "population" means something narrower than everyone. A partner that registers its whole addressable market as its population has rebuilt the open filter you were trying to close, and passes every check in that function. Review population size at renewal the same way you review the quota, and expire entries the partner no longer has a reason to hold.
The function covers three of five controls. The other two sit outside the request path.
Per-partner quotas tied to the declared purpose
When a partner signs up to verify addresses for 40,000 customers, the quota is a function of that number, not of your cluster's headroom. Set it at onboarding, review it at renewal, and make exceeding it a 429 rather than a line on an invoice.
Queries scoped to a declared population
Denmark's law already required the group to be identified individually in advance. The question for anyone building this is whether that requirement is enforced at query time by the API or lives only in the contract. A contract does not reject a request. A query-time check against the company's declared customers would have refused most of those 14 million lookups, since by the CPR office's own account they far exceeded its customer base.
Alerting on authorized but abnormal reads
Watch for an authorized partner behaving unlike itself: a volume step change against its own 30-day baseline, or a hit rate that drops toward what guessing looks like. Both are computable from the audit stream you're already writing. Route the alert to the person who owns the partner relationship, not to a dashboard nobody opens.
An egress audit trail the data owner can review
Record every record that left, attributed to the grant, the purpose and the person, in a stream you can hand to a regulator or to the subject. Datatilsynet's questions in this case (what happened, how it was possible, who is responsible for the processing) are answerable in an afternoon or not at all, depending on whether that trail exists. We've argued before that audit logs are a product feature, not a compliance checkbox; this incident is the argument in practice.
Named partner users who lose access when they leave
The CPR administration's follow-up points at the partner's side. In the week after disclosure, it wrote to the 1,039 companies with CPR access to reinforce its security rules: each must have a designated security officer, continuously check which employees have access, and track its monthly CPR transactions. If people at your partners reach your API through a console or through credentials tied to them, make those people individual users, signed in through the partner's own identity provider with MFA, and remove them when that directory says they've left. A shared admin login with a password nobody has changed is invisible from the API side until something like this happens.
The objection to all of this is that quotas break legitimate work. They do. A partner doing a one-time backfill will hit a purpose-scoped limit on day one, and if your only answer is a support ticket that takes a week, partners route around you by splitting traffic across keys. Build the raise path before you build the limit: a documented request flow, a named reviewer, a logged approval with an expiry. A quota with no sanctioned way up is a quota that gets evaded. The cost of skipping the tier shows up in the Danish timeline, where the only response left was to cut the company off twelve days after the lookups stopped.
Can a CPR number prove who someone is?
The other instruction out of Copenhagen is for everyone on the consuming side of identifiers like this. Laila Reenberg, director of Denmark's Agency for Public Security (Samsik), told companies and authorities to stop accepting a CPR number on its own as proof of identity, and said pharmacies that accept the number alone must find other ways. Samsik recommends MitID, two-factor authentication or one-time codes instead.
A national ID number is a join key. It identifies a row; it proves nothing about who is holding it. A leaked password can be rotated in an afternoon, but the 8.8 million people in this breach keep the identifier they had in September, which is why every system treating that number as a secret inherits the exposure of every other system that holds it. Note what held in Denmark: the minister said the breach did not reach systems that use MitID, the national digital login. Proof of possession held. Knowledge of a static string did not. The ministry's advice to citizens lands in the same place: never hand over passwords or other confidential information by phone or email, even if the caller already knows your name, address and CPR number.
Look at your own partner integrations this week and ask two questions. If one of them started pulling ten times its usual volume tomorrow, what would notice, and would the first system to notice be the one that sends the bill?
If people at your partners sign in to manage their integration, WorkOS SSO puts them behind their own company's identity provider, and Directory Sync removes them when that directory says they've left, so a former employee's login stops working on your side too. WorkOS Audit Logs records each event with a typed actor, so a partner's reads can sit in their own lane in the trail you hand a regulator.