API design for sensitive and settlement data
Designing an API that handles PII, financial records, or settlement data isn't the same as building a standard CRUD service. The stakes are different. A bug in your blog API shows the wrong post. A bug in your settlement API sends the wrong amount to the wrong account. The design principles shift when the data is sensitive.
Field-level encryption and data masking
Most teams encrypt data at rest and in transit, and call it done. But when you're dealing with sensitive payloads, field-level encryption is worth the complexity.
The idea is simple: instead of encrypting the entire row in your database, you encrypt individual fields — SSN, account number, routing number — with their own keys. This means a developer who has access to query the database still can't read the encrypted fields without the decryption key. It's defense in depth.
For API responses, data masking is essential. Your internal service might store a full account number, but your API should return ****4521. The masking should happen at the serialization layer, not as an afterthought. I build it into the response DTOs so it's impossible to accidentally leak a raw value.
If your API can return unmasked sensitive data without an explicit
include_sensitive=trueflag and elevated permissions, your default is wrong.
Audit trails and immutability
Every read and write of sensitive data needs an audit trail. Not just "who changed what" but "who viewed what." In financial systems, being able to prove that only authorized users accessed settlement records is a regulatory requirement, not a nice-to-have.
I design audit logging as a first-class concern, not middleware bolted on later:
- Every API endpoint that touches sensitive data emits an audit event.
- Events include the actor, timestamp, action, resource ID, and which fields were accessed or modified.
- Audit records are append-only. Nobody can delete or modify them. I typically write these to a separate data store with restricted access — sometimes an immutable ledger service.
For settlement data specifically, immutability is critical. Once a settlement record is finalized, it should never be mutated. If a correction is needed, you create a new adjustment record that references the original. This gives you a complete history and makes reconciliation possible.
Access control patterns
Role-based access control (RBAC) is the starting point, but it's often not granular enough. A "settlements admin" role might need to view settlement summaries but shouldn't see individual transaction details.
Attribute-based access control (ABAC) gives you more flexibility. Instead of just checking the user's role, you evaluate attributes of the request: the user's department, the sensitivity classification of the data, the time of day, whether the request is coming from a trusted network.
In practice, I combine both:
- RBAC for coarse-grained access — can this user access the settlements API at all?
- ABAC for fine-grained control — can this user see unmasked account numbers for settlements over $10,000?
The key is making the access control declarative and centralized. If access rules are scattered across controller methods, someone will forget to add a check. I use policy objects or a dedicated authorization service that every endpoint calls before returning data.
Versioning sensitive APIs
API versioning is annoying in general. Versioning APIs that carry sensitive payloads is worse.
When you introduce a new version that changes the shape of a sensitive field — say, moving from a flat account_number to a nested account.number — you need to support both versions simultaneously during migration. That means your encryption, masking, and audit logic all need to handle both shapes. It compounds fast.
My approach:
- Version at the resource level, not the entire API.
/v2/settlementscan coexist with/v1/transactionswithout forcing a full migration. - Deprecate aggressively but migrate carefully. Give consumers a clear timeline, but don't rush them. A consumer sending sensitive data to a deprecated endpoint is still your problem.
- Never change encryption schemes between versions without a migration plan. If v1 used AES-256-CBC and v2 uses AES-256-GCM, you need a path to re-encrypt existing data or support dual decryption.
- Keep the audit trail continuous across versions. A version change shouldn't create a gap in your audit history.
Design for distrust
The underlying philosophy for sensitive data APIs is design for distrust. Assume every consumer might mishandle the data. Assume every internal service might log the response. Assume someone will try to access data they shouldn't.
That means: mask by default, encrypt at every layer, log every access, version carefully, and make the secure path the easiest path. If doing the right thing requires extra effort from your consumers, they'll eventually cut corners. Make security the default, not the opt-in.