Skip to content

How to Ensure Zero Data Retention When Processing Third-Party API Payloads

Architect a pass-through unified API with zero data retention, regional residency guarantees, PII masking, and published retention windows for enterprise audits.

Roopendra Talekar Roopendra Talekar · · 43 min read
How to Ensure Zero Data Retention When Processing Third-Party API Payloads

Your enterprise deal just died in procurement. The buyer's InfoSec team found that your integration vendor caches their HRIS records and CRM contacts on shared infrastructure for 30 to 60 days. They flagged it as an unmanaged sub-processor, refused to sign, and moved on.

If you're processing third-party API payloads containing sensitive CRM, HRIS, or financial data, storing that data at rest is a massive liability. Enterprise procurement teams will actively block your deals if your integration architecture relies on caching their regulated data on unverified third-party infrastructure. To pass strict InfoSec reviews and ship integrations fast, you need an architecture that processes data in transit without ever writing it to a database.

This guide breaks down exactly how to architect a Zero Data Retention (ZDR) integration pipeline - why legacy sync-and-store architectures fail enterprise security audits, how to build a stateless pass-through proxy, and how to use declarative mapping languages to normalize payloads entirely in memory.

The Enterprise Procurement Wall: Why Data Retention Kills Deals

When you sell B2B SaaS to mid-market companies, integration velocity is your primary bottleneck. When you move your SaaS integration strategy upmarket to enterprise, compliance becomes the binary go/no-go for revenue.

Your account executive moves a six-figure deal to the final stages, and the buyer's procurement team sends over a Standardized Information Gathering (SIG) questionnaire - a structured risk assessment published by Shared Assessments. SIG Core is an extensive assessment with over 600 questions covering 21 risk categories, designed to assess third parties that store or manage highly sensitive or regulated information such as payment card data or genetic records.

Domain 10 - Third-Party Risk Management - acts as a tripwire. One question in particular will stop your deal cold: "Does any third-party sub-processor store, cache, or replicate our data?"

If your application relies on a middleware vendor that caches your customer's data to handle API retries or pagination, your answer is yes. That yes triggers a cascade of follow-up questions about data residency, encryption at rest, retention policies, breach notification timelines, and sub-processor agreements. You need an integration tool that doesn't store customer data. If that vendor refuses to sign a Business Associate Agreement (BAA) or lacks the required compliance certifications, the deal stops dead.

The financial stakes driving this scrutiny are massive. IBM's 2024 Cost of a Data Breach Report found the global average cost of a data breach reached a record $4.88 million - a 10% increase from 2023 and the largest spike since the pandemic. In the United States specifically, the average breach cost leads the world at $9.36 million per incident.

Enterprise InfoSec teams aren't being paranoid. They're doing math. Every additional sub-processor that stores sensitive data is another node in the blast radius of a potential breach. You are trading a short-term engineering convenience for a permanent compliance blocker.

What is Zero Data Retention in API Processing?

Zero Data Retention (ZDR) in API processing is an architectural pattern where payload data is processed entirely in-memory and immediately discarded, ensuring no sensitive information is ever written to disk, databases, or secondary storage logs.

For third-party integration platforms, ZDR means:

  • In-memory processing: All data transformations, filtering, and mapping occur in volatile memory (RAM) and are wiped immediately after the HTTP response is sent.
  • No persistent caching: The system does not write payload data to Redis, Memcached, or database tables to handle pagination or rate limiting.
  • No retry queues on disk: Failed requests are handled by the client or via stateless queuing systems that only store metadata, not the payload itself.
  • Redacted logging: API logs only capture metadata (status codes, latency, endpoint URLs) and actively strip request/response body payloads.

This isn't a new concept. Major technology providers are already shifting toward this model to satisfy enterprise demands. OpenAI offers Zero Data Retention for approved enterprise API customers, ensuring data is processed in-memory and not used for training - inputs and outputs are never logged and are not retained for application state. Brave Search API markets itself as the only search API offering true ZDR to reduce liability exposure for AI companies. LandingAI provides a ZDR option where documents are processed in-memory and immediately discarded. Your integration layer must meet this same standard.

ZDR also aligns directly with legal requirements. Article 5(1)(c) of the General Data Protection Regulation (GDPR) states that personal data shall be "adequate, relevant and limited to what is necessary in relation to the purposes for which they are processed" - a principle known as data minimization. If the purpose of your integration layer is to transform and relay data, then storing that data exceeds what is necessary.

Info

ZDR vs. data minimization: ZDR is the architectural implementation of the GDPR's data minimization principle applied to API middleware. Data minimization says "don't collect more than you need." ZDR says "don't persist anything at all - process it in transit."

ZDR Guarantees at a Glance

A compliant zero data retention pipeline enforces five invariants. If any one breaks, you don't have ZDR - you have a short-TTL cache dressed up with marketing:

# Guarantee What It Means How to Verify
1 No payload persistence Customer data from third-party APIs never touches databases, file systems, or object storage Audit every write path in your proxy layer. Grep for disk I/O on payload variables.
2 No disk spill under memory pressure Memory exhaustion triggers HTTP 413/503 rejection, not swap-to-disk fallback Load test with oversized payloads and confirm no temp files appear on the host.
3 No full-body logging Logs contain only metadata: status codes, latency, endpoint paths, error types Search your log aggregator for PII patterns (SSNs, emails, phone numbers) after running production traffic.
4 Stateless request lifecycle Each request is independent with no server-side session state or cross-request caches Restart the proxy mid-pagination and confirm the client resumes from its cursor.
5 Client-owned pagination state Page cursors pass through to the caller; the proxy stores nothing between requests Inspect response payloads and confirm next-page tokens are returned, not stored server-side.

If your integration vendor can't demonstrate all five, press them on what "zero data retention" actually means in their architecture.

The Flaws of Traditional Sync-and-Store Integration Architectures

Most integration platforms - whether built in-house or purchased from a vendor - follow a sync-and-store pattern:

flowchart LR
    A[Third-Party API<br>e.g. Salesforce] -->|Pull data| B[Integration<br>Middleware]
    B -->|Store in| C[(Middleware<br>Database)]
    C -->|Serve from cache| D[Your Application]

When you request a list of contacts from Salesforce through a traditional aggregator, the aggregator does not just proxy your request. It continuously polls the Salesforce API in the background, downloads the customer's entire CRM database, normalizes the data into its own proprietary schema, and stores it in a massive multi-tenant database. When you query their API, you're actually querying their cached copy of your customer's data.

This architecture has real engineering benefits - it reduces latency, avoids third-party rate limits, and makes pagination trivial. But it creates severe compliance problems:

Problem What Happens
Sub-processor expansion The middleware vendor becomes a data processor under GDPR and must be listed as a sub-processor in your DPA. You must now explain to enterprise InfoSec why a third-party startup holds 30 to 60 days of their employee records.
Data residency violations Customer data may be stored in regions that violate contractual or legal requirements.
Data staleness and retention ambiguity You're querying a cache, so data is inherently stale. If a user deletes a sensitive record in the source system, that record might persist in the aggregator's database for weeks until the next sync cycle completes.
Security honeypots Centralizing thousands of companies' CRM and HRIS data into a single multi-tenant database creates a high-value target for attackers.
Audit complexity You now need to audit your vendor's security controls - encryption at rest, access policies, breach notification - in addition to your own.

The worst part? Many teams don't realize the compliance cost until their first enterprise deal is on the line. By then, ripping out a deeply embedded sync-and-store integration layer is a multi-quarter project.

Passing enterprise security reviews when using third-party API aggregators is nearly impossible when the vendor's architecture fundamentally violates data minimization principles.

How to Architect a Pass-Through API Proxy

To achieve true Zero Data Retention, you must decouple the configuration of the integration from the execution of the payload. The architecture must act as a stateless proxy layer that translates requests on the fly.

Here is how a pass-through execution engine handles a unified API request without storing data at rest:

graph TD
    Client[Client Application] -->|Unified Request<br>GET /crm/contacts| Proxy[Stateless Execution Engine]
    
    subgraph Zero Storage Boundary
        Proxy -->|1. Load Config| DB[(Configuration DB<br>No Payload Data)]
        DB -->|Returns JSONata Mapping| Proxy
        Proxy -->|2. In-Memory Transform| Proxy
        Proxy -->|3. Native Request<br>GET /services/data/v59.0/query| Provider[Third-Party API<br>Salesforce, HubSpot]
        Provider -->|4. Native Response| Proxy
        Proxy -->|5. In-Memory Transform| Proxy
    end
    
    Proxy -->|6. Unified Response| Client

Here are the technical requirements for making this work:

1. Configuration as Data, Not Code

Instead of writing integration-specific code (if provider == 'salesforce'), the system stores the blueprint of the API - base URLs, authentication schemes, pagination strategies, and rate limit rules - as a JSON blob in a configuration database. The database contains zero integration-specific columns and zero payload data.

This is a critical distinction: operational metadata (tokens, refresh schedules, account configuration, JSONata expressions) lives in the database, while customer payload data (the actual CRM contacts, employee records, and financial transactions) never touches persistent storage.

2. Just-in-Time Credential Injection

When a request arrives, the engine retrieves the target account's encrypted credentials. It decrypts the OAuth token in memory, applies it to the outbound request header, and immediately discards the decrypted value. If the token is expired, the engine proactively refreshes it using a mutex lock to prevent race conditions, updates the encrypted storage, and proceeds with the request. The system must store OAuth tokens and API keys to function - that's unavoidable. But there's a hard line between stored credentials and customer data flowing through the system.

3. In-Memory Payload Transformation

All data mapping - from the third-party's native schema to your unified schema - must happen in memory. No intermediate writes to a database or file system. The transformation engine receives a JSON object, applies a mapping function, and outputs a new JSON object. Each request is self-contained: the proxy doesn't remember previous requests and doesn't maintain a local copy of the third-party's data. Every request goes directly to the source API, gets the freshest data, and returns it.

4. Client-Side Pagination and Real-Time Rate Limiting

Traditional systems cache data to handle pagination. A pass-through proxy handles it dynamically. The configuration defines the pagination style (cursor, offset, link header). The engine extracts the next-page cursor from the third-party response, maps it to a unified cursor format, and passes it directly back to the client. The client holds the state, not the middleware.

Similarly, when the third-party API returns a 429 Too Many Requests, the engine detects the rate limit, extracts the Retry-After header, normalizes it, and passes the 429 directly back to the client. No retry queues persist the payload to disk.

5. No Full-Body Request Logging

This one catches people off guard. Your observability stack probably logs full request and response bodies by default. If those bodies contain employee SSNs or patient records, your logging infrastructure just became a data retention liability. A zero-storage architecture logs metadata (status codes, latency, error types) but strips or redacts payload content from all logs.

Warning

The honest trade-off: Pass-through architectures are slower than cached ones. Walking through 50 pages of a third-party API on every request adds real latency. If you need sub-100ms response times on integration data, a pure pass-through won't get you there. You'll need to cache data in your own infrastructure (where you control retention, encryption, and residency) rather than relying on middleware to do it. Some platforms offer opt-in synced data stores for exactly this reason - the key is that the default path should be zero-storage.

Using JSONata for Stateless Payload Transformation

The hardest part of a pass-through integration architecture isn't the proxying - it's the real-time data transformation. You need to convert Salesforce's FirstName to your unified first_name, translate HubSpot's filterGroups into a common query format, and normalize completely different pagination schemes - all without writing the data to disk.

If you write imperative code to handle this, you end up with sprawling, unmaintainable microservices and the dreaded if (provider === 'hubspot') { ... } pattern that scales linearly with the number of integrations. Every new provider means new code, new tests, new deployments. Instead, you need a declarative transformation language.

Truto's zero-code architecture relies heavily on JSONata - a functional query and transformation language purpose-built for reshaping JSON objects. JSONata is ideal for ZDR because it is:

  • Side-effect free: Expressions are pure functions. They take an input, apply a transformation, and return an output without mutating external state or writing to disk.
  • Turing-complete: Despite being declarative, JSONata supports complex conditionals, string manipulation, array transforms, and custom functions necessary for mapping convoluted enterprise APIs.
  • Storable as configuration: A JSONata expression is just a string. It can live in a database column alongside integration configuration data, which means adding support for a new API is a configuration change, not a code deployment.

Here's what a stateless field mapping looks like for a Salesforce contact:

/* Transform a Salesforce contact into a unified schema */
{
  "id": Id,
  "first_name": FirstName,
  "last_name": LastName,
  "email": Email,
  "phone": Phone,
  "company_name": Account.Name,
  "created_at": CreatedDate,
  "updated_at": LastModifiedDate,
  "remote_data": $
}

And here's a more complex example mapping a HubSpot contact with custom date formatting:

(
  $formatDate := function($date) { $substring($date, 0, 10) };
  {
    "id": id,
    "first_name": properties.firstname,
    "last_name": properties.lastname,
    "email": properties.email,
    "phone": properties.phone,
    "created_at": $formatDate(createdAt),
    "updated_at": $formatDate(updatedAt),
    "remote_data": $
  }
)

Both expressions run entirely in memory. The engine evaluates the expression against the incoming payload, constructs the unified JSON object, sends it to the client via the open HTTP connection, and garbage-collects the memory. The data never touches a hard drive.

Declarative mappings scale as data - far easier to manage and audit than imperative code sprawl. Each new integration is a new configuration row, not a new microservice.

Tip

Handling Complex Orchestration Statelessly Sometimes a single unified request requires fetching data from multiple third-party endpoints (e.g., fetching a contact, then fetching their associated company). A pass-through engine handles this via "Before" and "After" pipeline steps. The engine executes the first request, holds the partial result in memory, executes the second request, merges the results using JSONata, and returns the final payload. No intermediate results are written to storage.

Normalizing Provider Error Responses In-Memory

Field mapping is the obvious use case, but a ZDR pipeline also needs to normalize error responses in memory. Many third-party APIs break HTTP conventions in ways that will confuse your application's error handling if left unprocessed.

Slack returns 200 OK for errors, signaling failure in the response body with { "ok": false }. Freshdesk returns 429 Too Many Requests when the customer's plan doesn't include API access - not an actual rate limit. A ZDR proxy must correct these inconsistencies in memory before the response reaches your client.

With JSONata, error normalization is a configuration change, not a code deployment:

/* APIs that return 200 for errors: detect body-level failure */
$not(data.ok) ? {
  "status": $mapValues(data.error, {
    "invalid_auth": 401,
    "token_expired": 401,
    "missing_scope": 403,
    "ratelimited": 429,
    "channel_not_found": 404
  }),
  "message": data.error
}
/* Remapping incorrect HTTP status codes:
   429 without a retry-after header means plan limitation, not rate limit */
status = 429 and $not($exists(headers.`retry-after`)) ? {
  "status": 402,
  "message": "API access is not available on this plan."
}
/* Non-JSON responses: detect errors in plain-text bodies */
$type(data) = "string" and $match(data, /Authorization Error/i) ? {
  "status": 401,
  "message": "Authorization error"
}

Each expression is stored as a string in the integration configuration database. During response processing, the engine evaluates the expression against the raw response in memory. If it matches, the error is normalized and returned. If it doesn't match, the expression returns undefined and processing falls through to standard HTTP status handling. No intermediate results touch disk.

Translating Query Parameters Across Providers

A unified API call like GET /crm/contacts?email=jane@acme.com needs to produce provider-specific query syntax. Salesforce wants a SOQL WHERE clause. HubSpot expects a filterGroups array. The translation runs entirely in memory:

/* Salesforce: unified filter to SOQL query string */
{
  "q": "SELECT Id, FirstName, LastName, Email FROM Contact"
    & (email ? " WHERE Email = '" & email & "'" : "")
}
/* HubSpot: unified filter to filterGroups request body */
{
  "filterGroups": [{
    "filters": [
      email ? {"propertyName": "email", "operator": "EQ", "value": email}
    ]
  }]
}

No query caches, no temporary tables. The mapping runs once per request, translates the parameters into the outbound API call format, and the result flows directly over the wire.

Concrete JSONata Transforms for PII Stripping

The mapping expressions below strip or mask sensitive fields for common SaaS object types before the payload leaves the transformation step. Every expression runs in memory, produces a new object, and leaves no artifact on disk. If you want the raw values dropped before they even reach your application, install the masking in the JSONata mapping rather than filtering downstream.

CRM Contact - Drop Free-Form Notes, Mask Phone

{
  "id": Id,
  "first_name": FirstName,
  "last_name": LastName,
  "email": Email,
  /* Keep only the last 4 digits of the phone number */
  "phone_last4": Phone ? $substring(Phone, $length(Phone) - 4) : null,
  /* Never expose free-form description or notes fields */
  "description": null,
  "internal_notes": null,
  "company_name": Account.Name,
  "created_at": CreatedDate,
  "updated_at": LastModifiedDate
}

HRIS Employee - Drop SSN, Mask DOB, Redact Bank Details

{
  "id": id,
  "employee_number": employee_number,
  "first_name": first_name,
  "last_name": last_name,
  "work_email": work_email,
  /* Personal identifiers dropped entirely */
  "ssn": null,
  "tax_id": null,
  /* Year-only date of birth for age-band analytics */
  "birth_year": date_of_birth ? $substring(date_of_birth, 0, 4) : null,
  /* Bank account replaced with a masked reference */
  "bank_account": bank_account ? {
    "last4": $substring(bank_account.number, $length(bank_account.number) - 4),
    "currency": bank_account.currency
  } : null,
  "department": department.name,
  "job_title": job_title,
  "employment_status": employment_status
}

Payment / Billing Record - Mask Card and Trim Metadata

{
  "id": id,
  "amount": amount,
  "currency": currency,
  "status": status,
  /* PCI card details reduced to brand + last 4 + expiry */
  "card": card ? {
    "brand": card.brand,
    "last4": card.last4,
    "exp_month": card.exp_month,
    "exp_year": card.exp_year
  } : null,
  /* Billing address city and country only, no street */
  "billing_location": billing_address ? {
    "city": billing_address.city,
    "country": billing_address.country
  } : null,
  "created_at": created
}

Support Ticket - Strip Message Bodies

{
  "id": id,
  "subject": subject,
  "status": status,
  "priority": priority,
  "requester_id": requester_id,
  /* Never propagate free-form ticket body or comment threads */
  "description": null,
  "comments": null,
  "created_at": created_at,
  "updated_at": updated_at
}

Healthcare / Patient Reference - Tokenized Identifier Only

{
  "id": id,
  /* Never expose MRN, diagnoses, or clinical notes through the unified layer */
  "patient_reference": $hash(patient_id),
  "appointment_time": appointment_time,
  "appointment_status": status,
  "provider_name": provider.display_name,
  "location": location.name,
  "diagnosis_code": null,
  "clinical_notes": null
}

The pattern is consistent across object types: enumerate the fields you want, set anything sensitive to null, and apply a mask function ($substring, $replace, $hash, or a custom helper) when you need to keep a partial value for lookup or audit purposes. Because each expression is stored as configuration, adjusting the masking policy for a specific integration is a config change - no redeploy required.

Handling Large Payloads: Streaming vs Buffering

Most SaaS API responses are small. A page of 100 CRM contacts might be 200KB of JSON. You can safely buffer that in memory, transform it with JSONata, and return it. But some payloads are large - bulk CSV exports, file attachments, audit log dumps spanning hundreds of megabytes. A ZDR proxy must handle both without touching disk.

Buffer When You Need to Transform

If the payload requires schema mapping, field renaming, or filtering, you need the full JSON object (or at least the current page) in memory. This is the standard case for API responses.

The pattern: buffer the upstream response body, parse it, apply the JSONata expression, serialize the result, and stream it to the client. Set hard caps on buffer size so a mislabeled bulk endpoint can't crash your process.

Stream When You're Passing Through Raw

For file downloads, binary attachments, or bulk exports where you don't need to inspect or transform the content, pipe the upstream response directly to the client without accumulating the body:

// Stream a file download without buffering the full body
app.get('/files/:id', async (request, reply) => {
  const upstream = await fetch(
    `https://api.provider.com/files/${request.params.id}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  reply.header('content-type', upstream.headers.get('content-type'));
  return reply.send(upstream.body); // pipe the ReadableStream directly
});

Peak memory usage is bounded by the chunk buffer (typically 16-64KB), not the file size. The proxy applies backpressure automatically - if the client reads slowly, the proxy slows its reads from upstream.

Page-at-a-Time Processing for Large Datasets

For large record sets, the third-party API already provides a natural chunking mechanism: pagination. Process each page independently instead of assembling the full dataset:

sequenceDiagram
    participant Client
    participant Proxy
    participant API as Third-Party API
    
    Client->>Proxy: GET /contacts
    Proxy->>API: GET /api/contacts?limit=100
    API-->>Proxy: 100 records + cursor_abc
    Note over Proxy: Transform in memory
    Proxy-->>Client: 100 unified records + next_cursor
    Note over Proxy: Memory released
    
    Client->>Proxy: GET /contacts?cursor=cursor_abc
    Proxy->>API: GET /api/contacts?cursor=cursor_abc
    API-->>Proxy: 100 records + cursor_def
    Note over Proxy: Transform in memory
    Proxy-->>Client: 100 unified records + next_cursor
    Note over Proxy: Memory released

The proxy's peak memory usage is one page of data, regardless of whether the full dataset is 1,000 or 1,000,000 records. The client holds the pagination state, not the server.

Memory and Resource Limits to Prevent Disk Spill

A ZDR proxy that runs out of memory and swaps to disk has silently broken its retention guarantee. The OS swap file, a temp directory, or a core dump now contains customer data on a physical drive. You need hard limits that cause requests to fail loudly rather than spill to storage.

Request and Response Size Caps

Set maximum body sizes at the HTTP server level. Reject anything larger with 413 Payload Too Large before the body is fully read:

Limit Recommended Range Why
Inbound request body 5-10 MB Covers most create/update payloads
Buffered response body 25-50 MB Handles large pages of records for transformation
Streaming chunk buffer 16-64 KB Bounds memory for pass-through file downloads

Make these configurable per integration. A HRIS API returning 50 employee records per page needs less headroom than a bulk export endpoint.

Timeouts at Every Layer

Long-running requests hold memory for their entire duration. Set aggressive timeouts and release buffers immediately when they fire:

Timeout Recommended Value
Upstream connect 5-10 seconds
Upstream response 30-60 seconds
Total request 90-120 seconds
JSONata evaluation 1-5 seconds

The JSONata timeout is easy to overlook. A malformed expression that creates an accidental infinite loop (or a $sort on a massive array) will consume memory until the process dies. JSONata's own guardrails support a timeout option in milliseconds, a stack depth limit, and a maximum sequence length - use all three.

Circuit Breakers for Misbehaving Upstreams

If a third-party API starts returning unexpectedly large payloads or hanging connections, a circuit breaker trips and fails fast:

  • Track error rate and response size anomalies per integration
  • Open the circuit after N consecutive failures or size limit violations
  • Return 503 Service Unavailable while the circuit is open
  • Half-open after a cooldown period and probe with a single request

Infrastructure-Level Safeguards

At the OS and container level:

  • Disable swap on proxy instances so memory exhaustion kills the process instead of writing to disk
  • Disable core dumps or route them to an encrypted ephemeral volume that's wiped on restart
  • Use memory-limited containers with hard ceilings that the runtime cannot exceed

How Pass-Through Zero-Retention Is Technically Enforced

Claiming zero data retention is easy. Proving it during an audit is where architectures break. Here are the specific mechanisms that keep customer payloads in memory only, plus what to show an auditor when they ask for evidence.

Memory Lifecycle of a Single Request

A ZDR request follows a bounded lifecycle. Every buffer must be reachable only inside the request handler's scope so the runtime's garbage collector can reclaim it once the response is flushed.

sequenceDiagram
    participant Client
    participant Handler as "Request Handler"
    participant Heap as "Process Heap"
    participant Upstream as "Third-Party API"

    Client->>Handler: Inbound request
    Handler->>Heap: Allocate request scope
    Handler->>Upstream: Native API call
    Upstream-->>Handler: Response bytes
    Handler->>Heap: Parse + transform in scope
    Handler-->>Client: Serialize + stream response
    Handler->>Heap: Scope exits, buffers unreferenced
    Note over Heap: GC reclaims memory<br>No disk write occurred

Concretely, that means:

  • No module-level caches holding payload data across requests. Only integration configuration and encrypted credentials sit in long-lived state.
  • No shared mutable state between concurrent requests. Each request builds its own transformation context.
  • Explicit buffer disposal in languages without automatic memory management. Zero out sensitive buffers before returning.
  • Streaming serialization where possible, so the response is written to the socket while the outbound object is still being constructed - never fully materialized twice.

No-Disk-Swap Guarantees

Memory pressure is where quiet retention happens. If the OS starts swapping to disk under load, decrypted tokens and payload bodies can land in the swap file. The mitigations:

  • Disable swap entirely on proxy hosts (swapoff -a, or MemorySwap=0 for container runtimes)
  • Set cgroup memory limits so containers OOM-kill before they cause node-level pressure
  • Reject requests early with 413/503 when buffer pools approach configured caps, instead of letting the runtime start paging
  • Disable core dumps (ulimit -c 0), or send them to a tmpfs mount that vanishes on reboot
  • Turn off transparent huge page defrag and any kernel feature that might page-cache secrets to disk
  • Pin ephemeral filesystems to tmpfs for any process-scratch directory so nothing survives a container restart

Proving It During an Audit

Auditors don't take your word for it. Provide evidence:

  • Syscall audit trail showing no writes to disk from proxy processes during load tests. Use strace -e trace=write,openat or the equivalent tracing on your platform.
  • Heap snapshots before and after handling sample payloads containing tagged canary values - the canary string should not appear in any subsequent snapshot or on-disk artifact.
  • Log samples with grep-style searches for PII patterns (email regex, SSN patterns, tagged canaries) coming up empty against production log volumes.
  • Storage inventory listing every database, bucket, and volume with a column for "contains payload data" - the answer should be "no" everywhere except opt-in synced stores.
  • Dependency review confirming no library in the stack (JSON parsers, HTTP clients, log formatters) spills to temp files during normal operation.

Data Residency in Unified APIs

Enterprise buyers in the EU, UK, Canada, and Australia will ask a specific version of the compliance question: "Where, geographically, does our data go when it passes through your integration layer?" This is the data residency question, and a pass-through unified API has a cleaner answer than sync-and-store platforms because most of the sensitive data never lands anywhere except the source system and your app.

Still, "cleaner" is not "none." You need to publish exactly where the metadata that does persist lives, what replication looks like, and which sub-processors touch each region.

Residency Model with Exact Guarantees

The table below separates the two categories a unified API touches: operational metadata (which must be persisted for the platform to function) and customer payload data (which flows through in memory only). Every row is a statement you should be able to defend in an audit.

Data Category Persisted? Residency Guarantee Encryption
Customer payload data (CRM records, HRIS employees, invoices) No N/A - never leaves the request lifecycle In-flight only (TLS 1.2+)
OAuth access tokens Yes Pinned to selected deployment region Envelope encryption with region-scoped keys
OAuth refresh tokens and API keys Yes Pinned to selected deployment region Envelope encryption with region-scoped keys
Integrated account configuration (provider, scopes, account ID) Yes Pinned to selected deployment region AES-256 at rest
Integration schema and JSONata mappings Yes Global (non-sensitive schema definitions, no customer data) Encrypted at rest
Webhook events awaiting delivery Transient Object-storage claim-check in selected region until delivered or expired Encrypted at rest until purged
Request/response logs Yes (metadata only) Selected deployment region Encrypted at rest, payload bodies redacted
Audit logs (admin actions, config changes) Yes Selected deployment region Encrypted at rest
Optional synced datasets (opt-in analytics stores) Yes Selected deployment region Encrypted at rest

Two things to notice. First, only one row - customer payload data - is what enterprise InfoSec is really asking about, and it's the one row marked "never leaves the request lifecycle." Second, everything else is pinned to a single region. Cross-region replication for disaster recovery should stay within a region group (EU-to-EU, US-to-US), never across geographies.

Region Hosting Map and Sub-Processor Locations

A unified API vendor should publish exactly which regions their platform runs in and which sub-processor categories sit in each region. Ask for this in RFP responses. A representative deployment map for a compliance-strict unified API looks like:

Region Group Typical Data Center Locations Sub-Processor Categories
North America Multiple US regions (e.g., us-east and us-west equivalents) Cloud IaaS provider, encrypted object storage, transactional database, log aggregation, monitoring
European Union Frankfurt, Dublin, or Paris regions EU-scoped cloud IaaS, EU-scoped object storage, EU-scoped databases, EU-scoped log aggregation
United Kingdom London region UK-scoped cloud IaaS, UK-scoped object storage
Asia-Pacific Sydney, Singapore, or Tokyo regions Regional cloud IaaS, regional object storage

Two guarantees to insist on when evaluating vendors:

  1. Regional isolation of stored data. Tokens, logs, and configuration for a customer provisioned in the EU deployment stay within EU-region infrastructure. Cross-region replication happens only within the region group for disaster recovery, never across geographies.
  2. Versioned sub-processor list per region. The vendor should publish a list of sub-processors (cloud provider, monitoring, error tracking, transactional email) with the specific regions each one operates in. Any change to that list must trigger DPA notice under GDPR Article 28(2).

For pass-through calls, the residency question has a subtlety. The proxy runs in the region you selected, but the third-party API you're calling (Salesforce, HubSpot, Workday) has its own residency. If your customer's Salesforce instance is in Frankfurt but you provisioned the unified API in the US deployment, the request path is: your app → US proxy → Frankfurt Salesforce → US proxy → your app. Payload data touches US infrastructure in memory only, but if your DPA restricts even in-flight transit, provision in the matching region so the whole path stays in-EU.

PII Masking in a Pass-Through Pipeline

Even in a ZDR architecture, some fields deserve extra care - free-form notes, medical fields, government IDs. A pass-through unified API can apply masking at three stages, all in memory:

1. Field-level redaction in the transformation. JSONata expressions can drop or partially mask fields before the payload leaves the transformation step:

{
  "id": Id,
  "first_name": FirstName,
  "last_name": LastName,
  "email": Email,
  /* Keep only the last 4 digits of a national ID */
  "national_id_last4": $substring(NationalId, $length(NationalId) - 4),
  /* Drop free-form notes entirely for this integration */
  "notes": null
}

2. Response filtering via query parameter. Callers can request that specific fields be excluded from the response body using a comma-separated exclude list. The proxy strips the fields in memory before serialization, so the excluded values never reach the outbound socket.

3. Log redaction. Structured logging middleware runs a redaction pass over any object that would otherwise be written to the log stream, matching known PII field names (email, ssn, phone, national_id) and configurable regex patterns before the log record is emitted. Any payload body that would have been logged is replaced with a size and hash.

The important distinction: PII masking in a pass-through pipeline is a transformation, not a storage control. Data still flows through memory to your application in unmasked form unless you explicitly configure masking. If you want the raw values dropped before they reach your app, add the masking to the JSONata mapping so the sensitive fields never make it out of the transformation step.

Retention Windows and Defaults for Metadata, Tokens, and Logs

Even in a strict ZDR architecture, some data must be persisted to run the platform: tokens, configuration, audit logs. Publishing exact retention windows for each category is what turns "we don't store your data" from a marketing line into an auditable statement.

Use the following defaults as a starting point. Every value should be configurable per environment so customers with stricter policies can override them.

Data Category Recommended Default Retention Notes
Customer payload data Zero Never persisted. Not "24 hours," not "as short as possible," not "until the next sync." Zero.
OAuth access tokens Provider-defined (typically 1-24 hours) Refreshed shortly before expiry; overwritten in place, never appended
OAuth refresh tokens Until connection revoked or replaced Rotated on each refresh where the provider supports rotation
API keys and static credentials Until connection revoked Encrypted at rest; never logged in plaintext
Integrated account configuration Until connection deleted Contains no payload data - provider, scopes, account identifier only
Request/response logs (metadata) 30 days Status codes, latency, endpoint, error class only - payload bodies redacted
Error events with stack traces 30 days Redact any variables that could contain payload data
Webhook events awaiting delivery Until successful ACK or max retry window (typically 24-72 hours) Object-storage claim-check purged after delivery
Audit logs (admin actions, config changes) 12 months minimum Required for SOC 2 evidence collection
Aggregated metrics (counts, latencies, percentiles) 13 months No PII - safe for long-term retention
Optional synced datasets Customer-configured (default: continuous) Opt-in only; deleted immediately on request

Every entry above answers a specific SIG Core question. When an auditor asks "How long do you retain OAuth tokens?", the answer is "For the lifetime of the connection or until rotated by the provider - typically hours for access tokens and until revocation for refresh tokens." No hand-waving required.

Two operational notes:

  • Deletion means deletion. When a customer disconnects an integrated account, tokens and configuration are hard-deleted (not soft-deleted with a deleted_at flag) within a defined window - 24 hours is a reasonable default. Backup copies should be purged on their normal rotation.
  • Retention overrides should be one-way. Customers can shorten retention windows below the defaults. They should not be able to extend log retention past a hard ceiling, because that turns your platform into their long-term storage vendor.

Quick-Start Redaction and Logging Configs

Log redaction is where most ZDR pipelines quietly fail. A single default console.log(req.body) in an error handler will ship an unredacted HRIS payload to your log aggregator. No-logs API payload processing is a discipline, not a checkbox - the configs below cover the three most common destinations and give you a starting policy for what metadata to keep and what to strip.

What to Log (and What Not to Log)

Every request should produce exactly one structured log line with these fields:

Field Example Why
request_id req_01HYZ... Correlate across services
trace_id 4bf92f3577b34da6... Distributed tracing
integration_slug salesforce Which provider
integrated_account_id ia_9k2f... (opaque UUID) Which connected account, without customer name
endpoint /crm/contacts Unified endpoint hit
method GET HTTP verb
status 200 Outbound response status
upstream_status 200 Third-party status
latency_ms 342 Total request time
request_bytes 1204 Inbound size (not body)
response_bytes 48211 Outbound size (not body)
payload_hash sha256:8d3f... For replay correlation (see next section)
error_class RateLimitError Category, not message body
retry_attempt 0 Retry sequence number
region eu-central Deployment region

Never log: request bodies, response bodies, Authorization headers, cookies, query strings that may carry tokens, or any field name matching email, ssn, phone, national_id, card, password, secret, token.

Datadog Agent Log Processing Rules

Agent-side scrubbing runs before logs leave the host, so it's the earliest place to redact:

# datadog.yaml or conf.d/<source>.d/conf.yaml
logs:
  - type: file
    path: /var/log/api-proxy/*.log
    service: api-proxy
    source: nodejs
    log_processing_rules:
      - type: mask_sequences
        name: mask_emails
        replace_placeholder: "[REDACTED_EMAIL]"
        pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
      - type: mask_sequences
        name: mask_ssn
        replace_placeholder: "[REDACTED_SSN]"
        pattern: '\d{3}-\d{2}-\d{4}'
      - type: mask_sequences
        name: mask_bearer
        replace_placeholder: 'Bearer [REDACTED]'
        pattern: 'Bearer\s+[A-Za-z0-9\-_.=]+'
      - type: mask_sequences
        name: mask_card_pan
        replace_placeholder: "[REDACTED_CARD]"
        pattern: '\b(?:\d[ -]*?){13,16}\b'
      - type: exclude_at_match
        name: drop_body_lines
        pattern: '"(request_body|response_body|payload)"\s*:'

Pair this with a Datadog pipeline processor that drops any log attribute named request_body, response_body, payload, or raw. Two layers: the agent redacts anything that slipped through as text, and the pipeline drops entire attributes if they still appear as structured fields.

Also disable APM's default body capture. In the tracer configuration, set DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING=false and confirm no integration has analytics_sample_rate combined with body tagging enabled. An APM tool ingesting full request bodies is a payload retention path.

ELK / Logstash Filter

For Elastic stacks, do the same work in a Logstash filter before shipping to Elasticsearch:

filter {
  # Drop known payload-bearing fields entirely
  mutate {
    remove_field => [
      "request_body", "response_body", "payload",
      "raw", "authorization", "cookie", "set-cookie"
    ]
  }

  # Mask common PII patterns in whatever's left
  mutate {
    gsub => [
      "message", "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", "[REDACTED_EMAIL]",
      "message", "\d{3}-\d{2}-\d{4}", "[REDACTED_SSN]",
      "message", "Bearer\s+[A-Za-z0-9\-_.=]+", "Bearer [REDACTED]",
      "message", "\b(?:\d[ -]*?){13,16}\b", "[REDACTED_CARD]"
    ]
  }

  # Drop any event whose message still looks like a JSON body
  if [message] =~ /^\s*\{.*"(email|ssn|phone|card|token)"/ {
    drop { }
  }
}

The final drop block is a defense-in-depth catch. If a body still shows up because a developer added ad-hoc logging, the event is dropped rather than indexed.

Splunk SEDCMD and Transforms

Splunk supports both index-time (props.conf / transforms.conf) and search-time redaction. Do index-time so raw events never land in the index:

# props.conf
[api_proxy]
SEDCMD-emails   = s/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/[REDACTED_EMAIL]/g
SEDCMD-ssn      = s/\d{3}-\d{2}-\d{4}/[REDACTED_SSN]/g
SEDCMD-bearer   = s/Bearer\s+[A-Za-z0-9\-_.=]+/Bearer [REDACTED]/g
SEDCMD-cardpan  = s/\b(?:\d[ -]*?){13,16}\b/[REDACTED_CARD]/g
TRANSFORMS-drop-bodies = drop_payload_bodies
 
# transforms.conf
[drop_payload_bodies]
REGEX = "(request_body|response_body|payload)"\s*:\s*"[^"]*"
FORMAT = "$1":"[REDACTED]"
DEST_KEY = _raw

The Splunk approach rewrites _raw before the event is indexed, so a search index=api_proxy request_body=* query returns only redacted values, not the original body.

Application-Level Redaction as the First Line

Aggregator-side redaction is a safety net. The primary redaction should happen in your application's logger:

// Pino redaction example
import pino from 'pino';
 
const logger = pino({
  redact: {
    paths: [
      'req.body', 'res.body', 'payload', 'raw',
      'req.headers.authorization',
      'req.headers.cookie',
      'req.headers["x-api-key"]',
      '*.email', '*.ssn', '*.phone', '*.card.*'
    ],
    censor: '[REDACTED]'
  }
});

If your primary logger already refuses to emit sensitive fields, the downstream Datadog/ELK/Splunk rules become insurance rather than the only defense.

Implementing a ZDR Proxy: Code Examples

The following examples show the core pattern: accept a request, fetch from the upstream API, transform the response in memory, and return it without writing anything to disk. They're minimal but production-shaped - add your own authentication, error handling, and observability on top.

Node.js (Fastify + JSONata)

import Fastify from 'fastify';
import jsonata from 'jsonata';
 
const app = Fastify({
  bodyLimit: 10 * 1024 * 1024, // 10 MB inbound cap
  requestTimeout: 90_000,
});
 
const UPSTREAM_TIMEOUT_MS = 30_000;
const MAX_RESPONSE_BYTES = 25 * 1024 * 1024;
 
app.get('/crm/contacts', async (request, reply) => {
  // JSONata guardrails: timeout, stack depth, max sequence length
  const mapping = jsonata(`$.records.{
    "id": Id,
    "first_name": FirstName,
    "last_name": LastName,
    "email": Email,
    "updated_at": LastModifiedDate
  }`, { timeout: 5000, stack: 500, sequence: 50000 });
 
  // Fetch upstream with a hard timeout
  const abort = new AbortController();
  const timer = setTimeout(() => abort.abort(), UPSTREAM_TIMEOUT_MS);
 
  try {
    const upstream = await fetch('https://api.provider.com/v1/contacts', {
      headers: { Authorization: `Bearer ${getToken(request)}` },
      signal: abort.signal,
    });
 
    // Enforce size limit before buffering the full body
    const raw = await upstream.text();
    if (raw.length > MAX_RESPONSE_BYTES) {
      return reply.code(502).send({ error: 'Upstream response exceeds size limit' });
    }
 
    // Transform in memory - no disk I/O
    const payload = JSON.parse(raw);
    const unified = await mapping.evaluate(payload);
 
    // Return directly; GC reclaims buffers after response
    return { data: unified, next_cursor: payload.nextPageToken ?? null };
  } finally {
    clearTimeout(timer);
  }
});

Python (FastAPI + httpx)

import httpx
from fastapi import FastAPI, HTTPException
 
app = FastAPI()
 
MAX_RESPONSE_BYTES = 25 * 1024 * 1024
UPSTREAM_TIMEOUT = 30.0
 
def transform_contact(record: dict) -> dict:
    """Pure function. No side effects, no disk I/O."""
    return {
        "id": record.get("Id"),
        "first_name": record.get("FirstName"),
        "last_name": record.get("LastName"),
        "email": record.get("Email"),
        "updated_at": record.get("LastModifiedDate"),
    }
 
@app.get("/crm/contacts")
async def get_contacts(cursor: str | None = None):
    async with httpx.AsyncClient(timeout=UPSTREAM_TIMEOUT) as client:
        params = {"cursor": cursor} if cursor else {}
        resp = await client.get(
            "https://api.provider.com/v1/contacts",
            headers={"Authorization": f"Bearer {get_token()}"},
            params=params,
        )
        resp.raise_for_status()
 
        if len(resp.content) > MAX_RESPONSE_BYTES:
            raise HTTPException(502, "Upstream response exceeds size limit")
 
        body = resp.json()
        unified = [transform_contact(r) for r in body.get("records", [])]
 
        # Returned directly over the wire. Nothing touches disk.
        return {"data": unified, "next_cursor": body.get("nextPageToken")}

Java (Spring WebFlux)

import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.*;
 
@RestController
@RequestMapping("/crm")
public class ContactsController {
 
    private static final int MAX_BUFFER = 25 * 1024 * 1024; // 25 MB
    private final WebClient client;
 
    public ContactsController(WebClient.Builder builder) {
        this.client = builder
            .baseUrl("https://api.provider.com")
            .codecs(cfg -> cfg.defaultCodecs().maxInMemorySize(MAX_BUFFER))
            .build();
    }
 
    @GetMapping("/contacts")
    public Mono<Map<String, Object>> getContacts(
            @RequestHeader("Authorization") String auth) {
        return client.get()
            .uri("/v1/contacts")
            .header("Authorization", auth)
            .retrieve()
            .bodyToMono(JsonNode.class)
            .timeout(Duration.ofSeconds(30))
            .map(body -> {
                // Transform in memory - pure function, no I/O
                List<Map<String, String>> unified = new ArrayList<>();
                for (JsonNode record : body.path("records")) {
                    unified.add(Map.of(
                        "id", record.path("Id").asText(),
                        "first_name", record.path("FirstName").asText(),
                        "last_name", record.path("LastName").asText(),
                        "email", record.path("Email").asText(),
                        "updated_at", record.path("LastModifiedDate").asText()));
                }
                return Map.of(
                    "data", unified,
                    "next_cursor",
                    body.path("nextPageToken").asText(""));
            });
    }
}

The patterns are identical across all three languages:

  • Hard size limits reject oversized responses before buffering completes
  • Timeouts prevent hung upstream connections from pinning memory
  • Pure transformation functions with no side effects and no I/O
  • No temp files, no caches, no database writes during the request lifecycle

Payload-Hash and Replay Troubleshooting Workflow

The single biggest operational objection to ZDR is: "How do we debug a failed integration if we don't have the payload?" The answer is a payload_hash on every log line, plus a well-defined replay workflow that reconstructs the payload from a re-executable source (your app, the third-party API, or the customer's environment) rather than from stored copies.

Computing and Logging a Payload Hash

For every inbound and outbound payload, compute a SHA-256 over the body and log the hex digest. The hash is a fingerprint - it identifies the payload uniquely for correlation, but it is not reversible and contains no PII.

import { createHash } from 'crypto';
 
function hashPayload(body) {
  const canonical = typeof body === 'string' ? body : JSON.stringify(body);
  return 'sha256:' + createHash('sha256').update(canonical).digest('hex');
}
 
// In the request handler
const requestHash = hashPayload(request.body);
const upstreamResponseHash = hashPayload(upstreamBody);
const outboundHash = hashPayload(transformed);
 
logger.info({
  request_id: request.id,
  request_payload_hash: requestHash,
  upstream_response_hash: upstreamResponseHash,
  outbound_payload_hash: outboundHash,
  status: 200,
  latency_ms: elapsed
}, 'request completed');

If a caller reports "the response was wrong for request req_01HYZ...", you can:

  1. Look up the log line by request_id
  2. Read the upstream_response_hash and outbound_payload_hash
  3. Ask the caller to re-run the same request and compute the hash of what they receive
  4. If the hashes match, the transformation is deterministic and you can reproduce locally with a synthetic payload; if they differ, either the upstream API returned different data on the two calls or the transformation config changed between them

Replay Workflow

Because the payload is not stored, "replay" means re-executing the request path rather than re-sending a saved body. The workflow:

sequenceDiagram
    participant Support as "Support Engineer"
    participant Logs as "Log Store (metadata only)"
    participant Replay as "Replay Endpoint (dev/staging)"
    participant Upstream as "Third-Party API"
    participant Customer as "Customer App"

    Support->>Logs: Fetch by request_id
    Logs-->>Support: Metadata + payload_hash
    Support->>Customer: Ask to re-issue request
    Customer->>Replay: Same unified request
    Replay->>Upstream: Native API call
    Upstream-->>Replay: Response body
    Replay->>Replay: Compute hash, transform
    Replay-->>Customer: Unified response + hash
    Customer-->>Support: New hash for correlation
    Note over Support: Hash match = deterministic bug<br>Hash mismatch = upstream drift

The replay endpoint should:

  • Only be enabled in development or staging environments, never production
  • Require explicit auth as an operator (not the customer's normal API key)
  • Log the replay attempt with the operator's identity as an audit event
  • Return the computed hash alongside the response so the operator can confirm correlation

Correlating Payload Hashes with External Systems

Since customers can also compute the hash of what they received (SHA-256 of the response body), you can correlate across systems without ever sharing the payload itself:

  • Customer side: "The response we got had hash sha256:8d3f..."
  • Proxy side: Log line for request_id shows outbound_payload_hash: sha256:8d3f...
  • Match: The customer received exactly what your proxy sent
  • Mismatch: Either the customer's own middleware modified the payload, or the wrong request_id was reported

The same technique works for webhook delivery. When a customer says "we didn't get the webhook for order 12345," you check the delivery log for the outbound hash, and either resend from the transient claim-check store (encrypted, purged after ACK) or ask the third-party to re-fire the event upstream.

What Not to Store for Debugging

Under pressure to solve a customer issue, it is tempting to enable "temporary" payload capture for a specific integration. Every temporary payload capture becomes a permanent retention exception unless it is:

  • Scoped to a single account or integration
  • Time-boxed with automatic expiry (24-72 hours)
  • Encrypted at rest with a customer-specific key
  • Logged as an audit event so the customer knows it was captured
  • Explicitly acknowledged in your DPA as an incident-response measure

The healthier default is deterministic reproduction via replay, not stored payloads.

Operational Checklist for ZDR in Production

Use this as a pre-launch gate. Every item should pass before you claim zero data retention in a security questionnaire or DPA.

  • Request body limits configured at the HTTP server level for both inbound and outbound payloads
  • Response size validation checks Content-Length and accumulated body size before full buffering
  • Upstream timeouts set per integration: connect, read, and total request timeouts
  • Transformation evaluation timeouts kill long-running JSONata (or equivalent) expressions before they exhaust memory
  • Swap disabled on all proxy instances, or containers enforce hard memory ceilings
  • Core dumps disabled or routed to encrypted ephemeral storage wiped on restart
  • Log redaction verified by searching production logs for PII patterns (SSNs, emails, phone numbers) after live traffic
  • Circuit breakers active for each upstream integration to fail fast on anomalous responses
  • No retry-with-payload queues - failed requests return errors to the client or retry statelessly with metadata only
  • Load tested under memory pressure with payloads at 2x expected maximum to confirm graceful 413/503 rejection
  • Pagination state externalized - cursors returned to the client, never stored server-side
  • Credential decryption is ephemeral - OAuth tokens decrypted in memory and discarded immediately after use
  • Dependency audit complete - no library in the dependency tree writes temp files during JSON parsing or HTTP handling
  • Region residency documented per data category, with a public sub-processor list per region
  • Retention windows published for tokens, logs, and audit trails, with hard ceilings enforced in code
  • Payload hashes logged on every inbound and outbound payload for replay correlation

When Zero-Retention Claims Are Invalid (and How to Disclose Caching)

The fastest way to lose an enterprise deal is to claim ZDR in a sales conversation and then have InfoSec discover a cache, a debug log, or a synced datastore that stores exactly the data you said you didn't. Overclaiming is worse than not claiming at all, because it turns a compliance conversation into a trust conversation.

Architectures That Cannot Claim ZDR

If your platform (or your vendor's platform) does any of the following on the default path, "zero data retention" is not an accurate description:

  • Sync-and-store aggregation. If you pull data from third-party APIs on a schedule and cache it in your own database to serve reads, you are storing customer payloads. Short TTLs do not change this - a 5-minute cache still holds customer data at rest, and a breach at minute 4 exposes it.
  • Persistent retry queues holding payloads. If failed requests are stored to a queue with the request body attached for later retry, the payload is at rest on disk. Metadata-only retry (store the request parameters, re-fetch from upstream on retry) is fine; body-carrying retry is not.
  • Debug or verbose logging enabled by default. If your logger emits request or response bodies in any environment, ZDR does not hold in that environment. "Debug mode is off in production" is a runtime toggle, not an architectural guarantee.
  • File attachments staged in object storage. If you buffer file uploads or downloads through object storage between the third-party and the client, those files persist for whatever the bucket's lifecycle policy says.
  • Long-lived webhook staging. Webhook events that sit in an object-storage claim-check for more than the delivery window are effectively cached payloads. Publish the exact TTL and enforce it in code.
  • Support-mode payload capture. If your support team can flip a switch to record payloads for a customer's traffic, that capture surface is a retention path. It must be documented, auditable, and off by default.
  • APM tools with body ingestion. APM tools that capture full request bodies for tracing (Datadog APM with body capture, New Relic with request bodies, Sentry with attached payloads) turn your observability stack into a payload store.

Safe Claims You Can Make in Sales and Security Reviews

Use precise language that survives an architecture review:

Instead of saying... Say...
"We have zero data retention" "Customer payload data is not persisted on the default pass-through path. Operational metadata (tokens, config, redacted logs) is persisted with published retention windows."
"We never store your data" "The proxy processes third-party payloads in memory only. Payloads are not written to databases, disk, or object storage during normal request handling."
"We're SOC 2 compliant so we're safe" "We hold SOC 2 Type II certification. Here is the report and the specific controls that cover data handling on the pass-through path."
"Nothing is logged" "Logs contain metadata (status codes, latency, endpoint, error class, payload hash) and are redacted for PII. Request and response bodies are not logged."
"Data never leaves your region" "Operational metadata for accounts in the [region] deployment stays in [region] infrastructure. Payload data flows through the [region] proxy in memory only, then to the third-party API and back to your app."

How to Disclose Opt-In Caching

If your product offers a synced-data feature, a data-warehouse export, or any other opt-in storage surface, disclose it clearly:

  1. Separate the default path from opt-in features in your DPA. The default unified API path and any opt-in cache should be described as distinct processing activities, with their own retention and residency terms.
  2. Require explicit customer configuration to enable persistence. Storage should never be the default. A customer should have to run a configuration step (API call, admin console toggle) to turn on a synced datastore.
  3. Publish retention and deletion behavior for the opt-in path. Include TTLs, deletion SLAs, and the geographic scope of the cache. Answer the same SIG questions for the opt-in path as you would for a sync-and-store vendor, because for that path, you are one.
  4. Log every enable/disable event as an audit action. A customer's compliance officer should be able to prove caching was off during a specific time window by pulling audit logs.
  5. Offer a hard "disable persistence" account setting. Some customers (regulated industries, EU public sector) need a guarantee that no one on their team can accidentally enable a cache. A tenant-level lock that prevents opt-in features from being turned on satisfies this.

Being honest about opt-in caching is not a weakness in a security review. It is what auditors want to hear. The vendors that lose deals are the ones who claim blanket ZDR and then have InfoSec find a cache in the architecture diagram.

Passing the SIG Core Questionnaire with a Zero-Storage Architecture

Let's bring this back to the deal stuck in procurement. Your enterprise prospect sent a SIG Core questionnaire. Here's how your answers change depending on your integration architecture:

SIG Question Category Sync-and-Store Answer Zero-Storage Answer
Does any sub-processor store customer data? Yes - middleware caches CRM/HRIS data for 30-60 days No - middleware processes data in transit only
Data residency controls? Depends on middleware vendor's infrastructure Yes - metadata pinned to selected region; no payload data at rest
Encryption at rest for stored data? Must verify middleware vendor's encryption practices Applies only to tokens, config, and logs - not payload data
Data retention and deletion policy? Must align with middleware vendor's retention schedule Published per data category; payload retention is zero
Breach notification for sub-processor? Must establish breach notification chain with middleware vendor Reduced scope - middleware has no payload data to breach

With a zero-storage architecture, you don't need to declare the integration vendor as a sub-processor for payload data. They still handle operational metadata (OAuth tokens, account configurations), but the sensitive customer data - the employee records, the financial transactions, the patient information - never touches their disk.

This doesn't make the questionnaire disappear. You still need to demonstrate that your own application handles data responsibly. But it removes an entire category of questions about third-party data handling, which is often the section that kills deals.

Where Truto Fits

Truto's unified API uses a pass-through proxy architecture by default. When your application calls GET /unified/crm/contacts, Truto calls the third-party API in real time, applies JSONata-based transformations in memory, and returns the unified response. No customer payload data is written to Truto's databases.

The architectural underpinning is a generic execution engine that reads declarative configuration (integration endpoints, auth schemes, pagination strategies) and declarative mappings (JSONata expressions for schema translation). A single code path handles every integration - Salesforce, HubSpot, Workday, and 100+ others - without any provider-specific logic in the runtime. This means there's no integration-specific database table or column where your data could accidentally end up.

Truto also offers regional deployments so that operational metadata - tokens, configuration, logs - stays pinned to the region you provision in. Payload data still flows through in memory only; the regional choice affects where the metadata about your integrations lives, not where your customers' records are stored (they're never stored).

When the enterprise auditor asks, "Does any third-party sub-processor store, cache, or replicate our data?", you can confidently answer "No." Truto processes the data in memory and routes it directly to your application, drastically simplifying SOC 2 and SIG Core audits.

Where the trade-off gets real: Truto also offers a synced data feature called SuperQuery for use cases that genuinely need cached, SQL-queryable integration data - like building dashboards or running analytics across thousands of records. This feature stores data. It's explicitly opt-in, and enabling it means you are introducing data persistence through Truto. The default Unified API path remains strictly pass-through.

According to IBM's same 2024 report, organizations making extensive use of AI and automation in security prevention workflows saw average breach costs reduced by $2.2 million compared to those that didn't. By adopting a zero-storage architecture, you eliminate the risk of cached data breaches entirely, protecting both your customers and your balance sheet.

If your primary concern is finding an integration tool that doesn't store customer data, the key question to ask any vendor is: "In the default API path, is customer payload data ever written to persistent storage - including logs, retry queues, and caches?" If the answer involves qualifications about retention windows, you're looking at a sync-and-store architecture dressed up with a short TTL.

FAQ for Security Reviewers and Auditors

The questions below come up in almost every SIG Core review, third-party risk assessment, and enterprise architecture review for a pass-through unified API. Use them as a template for your own vendor questionnaires or as a self-test before your next security review.

Q: Where is customer payload data stored? A: On the default pass-through path, payload data is not stored. It is received by the proxy, held in memory during transformation, and written directly to the outbound socket. No database write, no object storage, no disk cache. The only exception is opt-in synced datastores that the customer explicitly enables and configures.

Q: How do you handle retries without storing the payload? A: Inline retries happen within the request's memory scope. If the upstream returns a retryable error, the proxy re-issues the call using the parameters already in memory. Persistent retry (background retries after the request has returned) only applies to webhook delivery, where an encrypted claim-check is held for the delivery window and purged on ACK or expiry.

Q: What is logged for each request? A: Request ID, trace ID, integration slug, opaque account ID, endpoint path, HTTP method, status codes (upstream and outbound), latency, request and response byte counts, error class, retry attempt, region, and a SHA-256 hash of the payload. No headers with credentials, no query strings with tokens, no request or response bodies.

Q: How can you debug a customer issue if you don't have the payload? A: Via payload-hash correlation and deterministic replay. The customer re-issues the request in staging, we compute the hash and compare to the original log line. Matching hashes confirm the transformation is deterministic; mismatched hashes point to upstream drift or a customer-side modification.

Q: What happens under memory pressure? Does data spill to disk? A: No. Swap is disabled on proxy hosts, container memory limits are enforced, and requests are rejected with 413/503 before buffer pools approach the configured caps. Core dumps are disabled or routed to ephemeral tmpfs that is wiped on restart.

Q: Where does operational metadata (tokens, configuration, logs) live? A: Pinned to the deployment region you provisioned in. Cross-region replication for disaster recovery stays within a region group (EU-to-EU, US-to-US), never across geographies. See the residency table earlier in this document for the per-category breakdown.

Q: How long are OAuth tokens retained? A: Access tokens are refreshed shortly before expiry and overwritten in place (never appended). Refresh tokens are retained for the lifetime of the connection and rotated on each refresh where the provider supports rotation. On connection deletion, both are hard-deleted within 24 hours.

Q: How do you handle sub-processors? A: The platform's own sub-processors (cloud provider, monitoring, error tracking) are listed per region in the DPA and versioned so changes trigger Article 28(2) notice. Because payload data is not stored, our sub-processors do not act as processors for your customers' data - only for our own operational metadata.

Q: What certifications and reports can you provide? A: SOC 2 Type II report, GDPR DPA with sub-processor list per region, HIPAA BAA where applicable, and infrastructure evidence (syscall audit trails, heap-snapshot canary tests, log redaction validation) on request under NDA.

Q: What breaks the ZDR guarantee? A: Enabling any opt-in cached datastore, enabling debug logging with body capture, or configuring a support-mode payload trace. Each of these turns a persistent storage surface on for a specific scope. The default configuration has none of them active.

Q: How do we verify the ZDR claim during our own audit? A: We provide (a) architecture documentation showing every persistent store and its content type, (b) syscall trace results from load tests showing no writes to disk from proxy processes, (c) heap-snapshot canary tests showing tagged sensitive strings do not appear in any post-request memory snapshot or on-disk artifact, and (d) log redaction validation results showing PII patterns do not appear in production log volumes.

What to Do Next

If you're evaluating which integration tools are best for enterprise compliance, here's a concrete checklist:

  1. Audit your current architecture. Map every location where third-party customer data is persisted - databases, caches, log files, retry queues, analytics pipelines. You'll probably find more copies than you expected.

  2. Separate operational metadata from payload data. Your integration layer needs to store tokens and configuration. It should not need to store the actual data flowing through it.

  3. Ask vendors the hard questions. Request architecture diagrams showing exactly where data is written, a residency table by data category, and a per-region sub-processor list. Ask for their data flow documentation, not just their marketing page.

  4. Accept the latency trade-off - or own the cache yourself. If you need fast reads over integration data, cache it in your own infrastructure where you control retention, encryption, and residency. Don't outsource that responsibility to middleware.

  5. Validate with a real SIG Core dry-run. Before your next enterprise deal hits procurement, fill out the third-party risk management sections of a SIG Core questionnaire yourself. Identify the questions you can't answer cleanly and fix the architecture before it costs you revenue.

For a deeper walkthrough of the procurement process, see our guide on how to pass enterprise security reviews when using third-party API aggregators. For a detailed comparison of zero-storage platforms, read why Truto is the best zero-storage unified API for compliance-strict SaaS.

Stop letting your six-figure enterprise deals die in procurement. Architect your integration layer for compliance from day one.

FAQ

Where is customer payload data stored in a zero data retention API pipeline?
On the default pass-through path, payload data is not stored. It is received by the proxy, held in memory during transformation, and written directly to the outbound socket. No database write, no object storage, no disk cache. The only exception is opt-in synced datastores that a customer explicitly enables.
How do you debug a failed request if the payload is never stored?
Log a SHA-256 payload hash on every inbound and outbound payload. When a customer reports an issue, correlate by request ID and hash: ask them to re-issue the request in staging, compare the new hash to the original, and use deterministic replay to reproduce the transformation without needing a stored copy of the body.
What should you log for each API request in a no-logs payload processing pipeline?
Log metadata only: request ID, trace ID, integration slug, opaque account ID, endpoint, HTTP method, upstream and outbound status codes, latency, request and response byte counts, error class, retry attempt, region, and a payload hash. Never log request or response bodies, authorization headers, cookies, or query strings that may carry tokens.
When can you not claim zero data retention for third-party API payloads?
If your architecture uses sync-and-store aggregation, persistent retry queues with attached payloads, debug logging with body capture, long-lived webhook staging, support-mode payload trace, or APM tools that ingest full request bodies. Short TTLs on a cache do not qualify as ZDR.
How do you prevent data from spilling to disk under memory pressure?
Disable swap on proxy hosts, set cgroup memory limits so containers OOM-kill before node-level paging, reject oversized requests with 413/503 before buffer pools approach caps, disable core dumps or route them to tmpfs, and pin any process scratch directories to tmpfs so nothing survives a restart.
What's the difference between ZDR and data minimization under GDPR?
Data minimization (GDPR Article 5(1)(c)) requires that personal data be limited to what is necessary for the processing purpose. ZDR is the architectural implementation of that principle for API middleware: since the middleware's purpose is to transform and relay data, not store it, no persistence is necessary and the platform is designed to store nothing.

More from our Blog

What is a Unified API?
Engineering

What is a Unified API?

Learn how a unified API normalizes data across SaaS platforms, abstracts away authentication, and accelerates your product's integration roadmap.

Uday Gajavalli Uday Gajavalli · · 24 min read