A customer types an e-mail address into a checkout field. It goes into the shop's own order record, where it belongs, and a moment later the shop's server sends a check to Heatrail about that order. The address is not in the request, and there is no version of the request in which it could be. This article is about what travels instead, and what it protects.
The property
Raw e-mail addresses and phone numbers never reach Heatrail. What arrives is a keyed hash computed in the shop. What is stored is a second keyed hash computed only on the API, so the value in our database is not the value the shop sent. A hash of an e-mail address is still personal data, and we treat it as such.
Why it holds
Two stages on two machines
The first computation happens on the shop's own server. It takes the value the shop already holds, derives a keyed hash from it, and puts that hash in the request. The wording matters: this is not a promise that we discard the address after receiving it, and it is not a retention policy. The address never leaves the shop's infrastructure, so there is no misconfiguration on our side that could cause us to receive it, no debug flag that logs it and no support procedure that reads it back. A thing that never travels cannot arrive by accident.
The second computation happens only on the API, under key material that has never been anywhere else, and only its output is written down. So the values in our database are not the values any shop computed, and a copy of the database taken without that key material cannot be lined up against what shops send. The two stages exist for two different reasons. The first exists so that two shops can recognise the same identifier as each other without either of them showing it to us. The second exists so that our storage is not a set of values somebody else already holds.
Roles are kept apart on purpose, including one separation that is easy to miss: the credential a shop signs its requests with plays no part in identifier hashing. The constructions themselves, the field formats and what an integrator actually implements are documented where an integrator needs them, in the SDK reference and the API reference. This page is about the property, which is what stays true whichever way the mechanism is built.
Figure 1
Empty is refused, not hashed
Most of the usefulness of a hashed identifier comes from a dull assumption: two different people produce two different values. That assumption breaks the moment somebody hashes nothing.
The temptation is real and well meant. A shop with no phone number would like the payload shape to stay constant, so it hashes the empty string, or whatever placeholder the checkout drops into an untouched field, and sends that. The result is one value shared by every guest checkout in the network, which the system would read as a single extraordinarily busy customer tying thousands of unrelated orders together across dozens of shops. It would be the largest and most confident piece of nonsense in the graph, arrived at through a helpful convenience.
So it is refused on both sides. The client function returns nothing for an empty value, a whitespace value, a known placeholder and anything it cannot parse, and the contract is that the field is then omitted from the request entirely rather than sent as an empty string. The API separately refuses known placeholder values on arrival, because a contract only becomes a boundary once one side of it stops assuming the other got it right. A shop with no phone number sends no phone field. That is the whole of it.
What hashing buys, and what it does not
What it buys is narrow and worth having. Somebody who took everything we store would hold a set of values that cannot be read, cannot be mailed and cannot be called. A compromise of Heatrail does not yield a contact list. That is the sentence the whole arrangement exists to make true.
What it does not do is make the data anonymous. A shop that already holds a customer's address can compute the first-stage value for it and see whether that value is among the ones it sent us. That is not a weakness bolted onto the design, it is the same property that makes cross-shop recognition work at all: a value anybody with the original can reproduce is pseudonymous, not anonymous. It is also the reason every article in this library treats a hashed identifier as personal data throughout, and the reason hashing appears here as a safeguard rather than as an exemption from anything.
One boundary follows from the first stage, and the security page states it in the same words. That stage is keyed with material shared across participating shops, because shared material is exactly what makes one person's identifier look the same to two of them. It therefore sits, by construction, on every participating shop's server, so cross-shop recognition is as strong as the custody of that material across the network. The second stage is what keeps Heatrail's own storage out of that dependency: it runs on the API alone, under key material that has never been anywhere else.
Verify it
Two checks. The first fails before authentication matters, because the identifier field accepts a hash and nothing else: a raw address is a validation error rather than a value we take in and then promise to forget.
# Sign this the way the reference shows, and send a real Idempotency-Key:
# both are checked before the payload is, so a placeholder signature answers
# 401 and a missing key answers 400 rather than the error being demonstrated.
curl -s -X POST https://api.heatrail.eu/v1/check \
-H "Content-Type: application/json" \
-H "Authorization: HR1 {key id}:{signature}" \
-H "Idempotency-Key: chk-verify-hashing-boundary-01" \
-d '{"order_ref":"ord-1","ip":"203.0.113.42",
"email_h":"mari.tamm@example.ee"}'
# 422 invalid_payload. The details name email_h: the field takes a
# stage-one hash, and an address is not one.The second is a read rather than a call. The client is published code, so open its identifier hashing function and confirm what it does with nothing: an empty value, a whitespace value and a known placeholder all come back as null, and the documented contract is that the field is then left out. If it returned a value there instead, the network would acquire the enormous shared node described above, which makes it the one function on this page most worth reading yourself. It is in the SDK reference, and the same guarantee is stated for the API in the API reference.
Legal footing
Keyed hashing is a pseudonymisation measure in the sense of Article 4(5) GDPR, and an appropriate technical measure under Articles 25 and 32. It does not take the data outside the Regulation, which is precisely why every other article here treats hashed identifiers as personal data and why nothing on this site describes them as anonymous. Refusing raw contact fields at the API boundary is data minimisation under Article 5(1)(c), enforced by the schema rather than by policy: a field outside the accepted set is a validation error, so the minimisation cannot be undone by a well meaning integration. See the data processing agreement and the trust center.
Related
- Trails, on what these values are linked into once they arrive, and why that is a property of history rather than a record about a person.
- Why the network cannot be enumerated, on the second half of the same argument: a hashed identifier is little use to an attacker who also cannot ask us questions about it.
- Forgetting on schedule, on how long these values are kept and what deletes them.
- The SDK reference, for the mechanism this article deliberately describes only as a property.