Short answer: choose the delivery setup that can prove what data crossed each boundary, why it crossed it, and when it became useless. For a customer-support password reset, GDPR compliance is not a checkbox attached to an email provider. It is a flow-design problem.
The reset endpoint should create a short-lived, single-use capability, hand only the minimum message data to a delivery component, and record delivery events without recording the secret. That design gives a support team something useful during an investigation: a timeline and decision record, rather than a mailbox full of reset links.
How do I evaluate a GDPR-compliant password reset email service?
The email address is personal data in this context, and the reset token is security-sensitive data even if it is opaque. The application needs a documented purpose and a lawful basis for processing, appropriate security measures, and a retention rule. GDPR Articles 5, 6, 25, and 32 are the places I would start with counsel and the privacy owner; they connect purpose limitation, data minimization, privacy by design, and security of processing.
That does not mean the message must contain a privacy essay. It means the system should be able to answer narrower questions. Which service generated the request? Which region handled the message? Which template version was used? Was the token ever present in a log, event payload, support ticket, or analytics tool?
I keep the reset token inside the application boundary. The delivery payload contains a prebuilt, one-time URL only when the delivery boundary is explicitly approved; a stricter design gives the mail renderer a short-lived reference and resolves the URL inside the application. The second option adds a request hop and operational work. It also reduces the number of systems that can accidentally retain a credential. That is a real trade-off, not a compliance slogan.
The boundary map matters more than the provider badge
Draw the flow before comparing services:
type ResetDispatch = { requestId: string; recipient: string; templateId: string; expiresAt: string; locale: string; }; interface MessageTransport { send(message: ResetDispatch): Promise<{ accepted: boolean; providerMessageId?: string; }>; } This interface deliberately has no token field, subject override, or arbitrary metadata bag. Those omissions are protective. The application owns token creation, expiry, single-use enforcement, and invalidation. The transport owns delivery attempts and provider responses. A support console should receive requestId and status, never the reset URL.
There are four boundaries worth documenting: the reset service, the message renderer, the delivery processor, and the observability stack. For each one, write down fields, purpose, access role, storage location, transfer region, and deletion behavior. I put this in a small table during design review, then ask the same questions after deployment: can an on-call engineer identify the request without opening its secret, can a privacy reviewer see where recipient data went, and can the team delete the record without deleting evidence needed for a security incident? If the answer to any one of those questions depends on a dashboard export or an undocumented provider setting, the boundary is not finished. The list is boring. That is why it works.
Keep the reset secret boring, too.
SPF is one piece of the delivery boundary, not proof of GDPR compliance. RFC 7208 defines a mechanism for publishing authorized sending hosts in DNS. DKIM and DMARC address other parts of sender authentication and policy. These controls help protect delivery integrity and domain reputation; they do not decide your retention period, processor terms, or access model.
How should the system prove a reset message was safe?
Use an event record that is useful without becoming a second secret store. A minimal record can include a random request identifier, a keyed digest of the recipient for controlled correlation, template version, policy decision, timestamps, outcome class, and the delivery system's message identifier. Avoid the email body, URL, token, and full provider response.
The event sequence should be explicit:
- Accept the reset request without revealing whether the account exists.
- Create a token with a short expiry and enforce one-time use at redemption.
- Persist the minimum audit event before dispatch, with a request identifier.
- Send through a queue with bounded retries and an idempotency key.
- Record acceptance, rejection, bounce, or complaint as separate outcome classes.
- Delete or aggregate events when the documented retention period ends.
A queue retry must not create five valid tokens. The token belongs to the reset request, while delivery attempts belong to the message job. Those are different identities. I would test that distinction with a fake transport, a clock you can advance, and a mailbox sink that asserts the token is absent from logs. This is the kind of test that catches a subtle design error before support has to explain why the fifth email invalidated the first four. It also makes the delivery service replaceable: the reset state remains in one place while the transport changes around it.
What should be measured before choosing a service?
Start with delivery reliability, but define it more carefully than a single percentage. Measure acceptance latency, temporary-failure recovery, permanent-failure classification, and the time from a user request to a usable message. Split results by mailbox provider, region, language, and template version. A high aggregate acceptance rate can hide a painful failure for one support population.
Then test the operational controls that are easy to forget: exportable event history, role-scoped access, configurable retention, regional processing information, deletion support, and a processor agreement that matches the actual data path. Treat these as pass/fail gates. A polished API does not compensate for an evidence gap during a data-subject request or security review.
Do not put the reset URL into click tracking. If link inspection is required, document what it changes and test expiry, single use, and intermediate redirects. For support, the useful signal is usually “message accepted” or “message failed,” not a dashboard that records every path a credential-bearing URL took.
A sensible evaluation has three fixtures: an existing account, a nonexistent account, and a token that has expired. Run them through retries, duplicate requests, a temporary delivery failure, and a complaint event. Keep the fixture data synthetic. Record which system can see each field. That final column often changes the architecture more than a feature matrix does.
The decision rule
For this workflow, the right service is the one that fits the approved boundary and produces enough evidence to operate it. Vendor reach, template tooling, and SDK ergonomics matter after that. They should not decide where reset secrets, recipient data, or audit events live.
Prefer a design where the application remains authoritative for identity and token state, delivery is replaceable behind a small interface, and observability can answer operational questions without copying sensitive content. Recheck the data map whenever the template, tracking behavior, region, or support workflow changes.
That is the practical standard: a password reset should expire in the account system, disappear from logs by policy, and leave behind an audit trail that explains the delivery without exposing the credential.