Skip to content

Zero Data Retention AI Agent Architecture: Connecting to NetSuite & SAP Without Caching

Learn how to build a stateless, pass-through integration architecture that connects AI agents to enterprise ERPs like NetSuite and SAP without caching sensitive data.

Yuvraj Muley Yuvraj Muley · · 23 min read
Zero Data Retention AI Agent Architecture: Connecting to NetSuite & SAP Without Caching

If you are building a B2B SaaS product that gives AI agents read/write access to enterprise ERP data—NetSuite general ledgers, SAP purchase orders, Dynamics 365 financial records—and your integration middleware caches any of that data, you are building a compliance liability that will kill enterprise deals before your sales team can close them.

Enterprise software is undergoing a massive shift. Engineering teams are replacing static dashboards with autonomous agents capable of reconciling invoices, drafting purchase orders, and querying general ledgers. The challenge is no longer just building the AI model. The bottleneck is securely connecting that model to legacy enterprise systems.

Zero Data Retention (ZDR) architecture is the design pattern where your integration layer processes third-party API payloads entirely in memory, never writing them to persistent storage. The middleware acts as a stateless proxy: it authenticates, transforms, and forwards requests between your AI agent and the upstream ERP, then discards the payload the moment the response is delivered. No database writes. No log files containing financial records. No cached copies of your customer's chart of accounts sitting on infrastructure you now have to secure, audit, and defend in every procurement review.

This guide breaks down exactly why caching ERP data is an architectural mistake when AI agents are involved, how the Model Context Protocol (MCP) standardizes agent connectivity, and the specific engineering patterns—rate limit normalization, declarative data mapping, MCP tool generation—that make stateless ERP integration practical at scale.

The Enterprise Procurement Wall: Why Caching ERP Data Kills AI Deals

When you sell AI-powered software to enterprise buyers, your architecture dictates whether your product passes InfoSec procurement or dies in legal review.

Here is what actually happens when your account executive moves a six-figure deal to the final stages. The buyer's InfoSec team sends a Standardized Information Gathering (SIG) questionnaire—a structured risk assessment with versions ranging from SIG Lite (around 150 questions) to SIG Full (over 1,000 questions covering 20 risk domains). Developed and maintained by the Shared Assessments Group, the SIG is widely adopted by organizations across industries such as finance, healthcare, SaaS, and government contracting to conduct third-party vendor risk assessments.

The moment your architecture diagram shows a database between your agent and the customer's ERP, the questionnaire becomes exponentially harder to pass. You instantly become a high-risk vendor because you now own that data. You need encryption at rest, key rotation policies, data residency guarantees, breach notification procedures, and retention schedules for every record type you cache.

ERP systems contain a company's most sensitive information:

  • Protected Health Information (PHI): Invoice line items often contain PHI in healthcare contexts, triggering strict HIPAA exposure.
  • Financial Data: General ledgers contain unreleased financial data. If that data comes from a publicly traded company, you may trigger SOX implications.
  • Personally Identifiable Information (PII): Employee expense reports and HRIS records contain highly sensitive PII.

The financial exposure is not hypothetical. According to the IBM Cost of a Data Breach 2024 report, the financial sector has seen a surge in data breach costs since the pandemic, reaching an average of USD 6.08 million per incident. Breach costs increased 10% from the prior year, the largest yearly jump since the pandemic, as 70% of breached organizations reported that the breach caused significant or very significant disruption.

AI agents compound this risk. They are not static integrations that run a batch sync once a day. They make unpredictable, multi-step API calls across tool-calling chains, potentially touching invoices, vendor records, employee data, and journal entries in a single reasoning loop. According to research by Cyberhaven Labs, enterprise adoption of endpoint-based AI agents has grown by 276% over the past year, more than triple the growth rate of GenAI SaaS tools, signaling a swift shift toward autonomous systems that operate outside traditional security controls. Every cached payload from one of those agent interactions is a record you must now account for in your security posture.

The architectural choice is binary: cache the data and inherit the full compliance surface area of every customer's ERP instance, or build a pass-through layer that processes payloads in memory and keeps your compliance footprint close to zero. One path leads to SOC 2 scope creep and stalled revenue. The other keeps your compliance footprint small enough that InfoSec teams sign off in days.

What is Zero Data Retention (ZDR) Architecture?

Zero Data Retention architecture is a stateless pass-through design where third-party API payloads are processed entirely in memory and never written to any persistent storage—no databases, no disk caches, no log files containing business data.

Most unified API platforms and embedded iPaaS solutions rely on a "sync-and-store" model. They poll the third-party API, pull the data into their own managed databases, normalize it, and serve it to you via their own endpoints. This creates a massive, unnecessary replica of your customers' sensitive data. Sync-and-store reduces latency and API call volume, but it means your middleware now contains a copy of every record it has ever fetched—and you are responsible for securing, governing, and eventually deleting all of it.

ZDR flips this model. When your AI agent requests data from NetSuite, the request flows through a stateless execution engine. The engine translates the request into the native ERP format, fetches the data directly from the source, maps the response in memory, and returns it to your application.

The key properties of a ZDR integration layer:

  • In-memory processing only: Request and response payloads exist in volatile memory for the duration of the API call. Once the response is returned to the caller, the data is garbage-collected.
  • No payload logging: Operational logs capture metadata (timestamps, status codes, latency) but never the body of API responses containing business data.
  • Credential isolation: OAuth tokens and API keys are stored securely, but the business data those credentials unlock is never persisted.
  • Stateless request handling: Each API call is independent. The middleware does not maintain a local copy of ERP records between requests.
  • Real-Time Accuracy: AI agents require the absolute latest state of a system before taking action. Caching introduces latency and synchronization conflicts.
sequenceDiagram
    participant Agent as AI Agent<br>(LangChain/Claude)
    participant App as Your SaaS App
    participant Proxy as ZDR Proxy Layer<br>(In-Memory)
    participant ERP as Enterprise ERP<br>(NetSuite/SAP)

    Agent->>App: Request Invoice Status
    App->>Proxy: GET /unified/accounting/invoices
    Note over Proxy: Resolve credentials<br>Map to native format
    Proxy->>ERP: SuiteQL / REST Query
    ERP-->>Proxy: Raw JSON Response
    Note over Proxy: Transform via JSONata<br>Data kept in memory only
    Proxy-->>App: Normalized JSON
    App-->>Agent: Context for LLM

The trade-off is real: every request hits the upstream API, so you are subject to the provider's latency and rate limits. You cannot serve stale data from a local cache when the upstream is down. But for regulated industries—finance, healthcare, legal—that trade-off is overwhelmingly worth it.

Read more about why pass-through architectures win for AI agents.

Connecting AI Agents to NetSuite and SAP via MCP

The industry is moving rapidly toward standardizing AI connectivity. The Model Context Protocol (MCP) has rapidly become the standard interface for giving AI agents structured access to external systems. MCP is an open standard and open-source framework introduced by Anthropic in November 2024 to standardize the way AI systems like large language models integrate and share data with external tools, systems, and data sources.

Just one year after its launch, MCP has achieved industry-wide adoption backed by competing giants including OpenAI, Google, Microsoft, AWS, and now governance under the Linux Foundation. Over 1,000 live MCP connectors now cover enterprise platforms, allowing AI agents to interact with structured business systems through a governed interface.

For ERP integration specifically, MCP provides a governed control plane between the AI agent and the financial system. Instead of giving an LLM raw HTTP access to NetSuite's REST API (a terrifying proposition), an MCP server exposes a curated set of tools—list_invoices, create_purchase_order, get_account_balance—with explicit input schemas, permission boundaries, and audit hooks.

Enterprise ERP vendors are actively embracing this pattern. Oracle NetSuite recently announced the expansion of its AI Connector Service to let customers connect external AI assistants to ERP data in a governed, role-based framework. This is a shift toward treating AI agents as managed infrastructure rather than ad hoc tools.

The NetSuite Architecture Challenge

Exposing an ERP to an AI agent requires translating complex, fragmented APIs into clean, predictable MCP tools. NetSuite is a perfect example of why this is exceptionally difficult to build in-house.

Unlike simpler REST APIs, connecting an agent to NetSuite requires orchestrating across three distinct API surfaces, each with its own capabilities and limitations:

  1. SuiteTalk REST API: This is the primary surface, but standard REST CRUD operations are heavily restricted. To get meaningful data, agents must use SuiteQL (NetSuite's SQL-like query language) via POST /services/rest/query/v1/suiteql. SuiteQL enables multi-table JOINs across subsidiaries and currencies, which is required for accurate financial reporting.
  2. RESTlet (SuiteScript): Certain capabilities are simply impossible through REST or SuiteQL. For example, if your agent needs to download a Purchase Order PDF, it requires a custom SuiteScript Suitelet deployed into the customer's account utilizing the server-side N/render module.
  3. SOAP API: Legacy tax rate configurations are often missing from the modern SuiteQL tables. Fetching detailed sales tax item data requires falling back to the legacy SOAP getList operation.

A ZDR architecture abstracts this complexity. The AI agent simply calls a unified get_invoice MCP tool. The proxy layer dynamically routes the request to SuiteQL, handles the OAuth 1.0 HMAC-SHA256 signature generation in memory, and returns the unified result.

Why MCP + ZDR Is the Right Combination for ERP

MCP servers are the natural place to enforce zero data retention. The server sits between the agent and the upstream API, and it controls the entire request lifecycle. As highlighted in our comparison of MCP server data retention policies, if the MCP server is stateless—processing ERP payloads in memory and discarding them after tool execution—then the agent never triggers data persistence in the middleware layer.

The alternative is building custom LangChain or LlamaIndex tool functions that call ERP APIs directly. That works for prototypes, but it pushes authentication management, rate limit handling, error normalization, and data transformation into your application code. Every new ERP you support means another set of custom functions to build, test, and maintain.

A platform like Truto takes a different approach: it auto-generates MCP tools directly from integration configurations. Because every integration is defined as data—a JSON config describing the API surface plus declarative mapping expressions for data transformation—MCP tool definitions are derived automatically. The same configuration that powers the unified REST API also powers the MCP server, with no additional code per integration. And because the platform operates as a stateless proxy, ERP payloads are never persisted.

Step-by-Step: Giving AI Agents Access to NetSuite Without Caching Data

For ERP agents specifically, zero data retention means the agent never stores financial records, vendor data, employee information, or any other ERP payload outside of the in-flight request. The agent issues a tool call, the proxy fetches from NetSuite in real time, transforms the response in memory, returns it to the agent's context window, and discards the payload. No intermediate database. No cache layer. The agent's context window is the only place the data lives after the request completes.

This section walks through the full implementation: designing MCP tool schemas, routing tool calls to the correct NetSuite API surface, and verifying that no payloads touch persistent storage.

MCP Tool Design: JSON Schemas for Common NetSuite Operations

MCP tools are defined with explicit input schemas so the agent knows exactly what parameters are available and what each one does. These schemas are not hand-coded per integration - they are generated dynamically from the integration's resource definitions and documentation records. A tool only appears in the MCP server if it has a corresponding documentation entry, which acts as both a quality gate and a curation mechanism.

Here are example tool definitions for common NetSuite operations:

Listing invoices:

{
  "name": "list_all_netsuite_invoices",
  "description": "List invoices from NetSuite. Supports filtering by invoice type (bill or invoice), issue date range, and sorting by issue date, due date, or creation date.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "invoice_type": {
        "type": "string",
        "enum": ["bill", "invoice"],
        "description": "Filter by invoice type. 'bill' returns vendor bills (AP), 'invoice' returns customer invoices (AR)."
      },
      "issue_date_gte": {
        "type": "string",
        "description": "Filter invoices with issue date on or after this value (ISO 8601)."
      },
      "issue_date_lte": {
        "type": "string",
        "description": "Filter invoices with issue date on or before this value (ISO 8601)."
      },
      "sort_by": {
        "type": "string",
        "enum": ["issue_date", "due_date", "created_at"],
        "description": "Field to sort results by."
      },
      "limit": {
        "type": "string",
        "description": "The number of records to fetch."
      },
      "next_cursor": {
        "type": "string",
        "description": "The cursor to fetch the next page. Always send back exactly the cursor value you received (nextCursor) without decoding, modifying, or parsing it."
      }
    }
  }
}

Getting a single invoice by ID:

{
  "name": "get_single_netsuite_invoice_by_id",
  "description": "Get a single invoice from NetSuite by its internal ID. Returns full line item detail including amounts, descriptions, and accounting references.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "The internal ID of the invoice to retrieve. Required."
      }
    },
    "required": ["id"]
  }
}

Downloading a Purchase Order as PDF:

{
  "name": "netsuite_purchase_orders_download",
  "description": "Download a Purchase Order as a rendered PDF from NetSuite.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "The internal ID of the purchase order to download. Required."
      }
    },
    "required": ["id"]
  }
}

Getting tax rates (SOAP fallback):

{
  "name": "list_all_netsuite_tax_rates",
  "description": "List sales tax rates from NetSuite including tax type, rate percentage, and subsidiary assignments.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "limit": {
        "type": "string",
        "description": "The number of records to fetch."
      },
      "next_cursor": {
        "type": "string",
        "description": "The cursor to fetch the next page. Always send back exactly the cursor value you received (nextCursor) without decoding, modifying, or parsing it."
      }
    }
  }
}

Notice the pattern: list tools automatically include limit and next_cursor parameters for pagination, while get, update, and delete tools have id injected into the schema. The next_cursor description explicitly instructs the LLM to pass cursor values back unchanged - a small but important detail that prevents agents from trying to decode or manipulate opaque pagination tokens.

Proxy Routing: Mapping Tools to SuiteQL, RESTlets, and SOAP Fallbacks

When the MCP server receives a tools/call request, the proxy layer must route it to the correct NetSuite API surface. This routing is driven entirely by the integration configuration - there is no hardcoded switch statement per operation.

flowchart TD
    A["MCP tools/call<br>e.g. list_all_netsuite_invoices"] --> B["Proxy Layer<br>(reads integration config)"]
    B --> C{"Route by<br>resource + method"}
    C -->|"Invoices, Contacts,<br>Expenses, Journal Entries"| D["SuiteQL<br>POST /query/v1/suiteql"]
    C -->|"PO PDF Download,<br>Field Metadata"| E["RESTlet / Suitelet<br>Custom Script Endpoint"]
    C -->|"Tax Rates<br>(detail enrichment)"| F["SOAP getList<br>/NetSuitePort_2020_2"]
    C -->|"Create PO,<br>Delete Vendor"| G["REST Record CRUD<br>/record/v1/{type}/{id}"]
    D --> H["In-Memory Transform<br>(JSONata)"]
    E --> H
    F --> H
    G --> H
    H --> I["MCP Response<br>(JSON-RPC 2.0)"]

The routing logic breaks down like this:

Operation NetSuite API Surface Why
List/Get invoices, contacts, expenses, journal entries, accounts SuiteQL via POST /query/v1/suiteql Enables multi-table JOINs across subsidiary, currency, and entity tables in a single call
Create purchase order, delete vendor REST Record API via /record/v1/{type}/{id} SuiteQL is read-only - all writes require the REST record API
Download PO as PDF RESTlet / Suitelet (custom SuiteScript) REST API has no PDF rendering capability - requires the server-side N/render module
Get dynamic form field metadata RESTlet / Suitelet (custom SuiteScript) REST metadata catalog lacks runtime form state (field visibility, select options, mandatory flags)
List tax rates with full detail SuiteQL + SOAP getList SuiteQL returns basic tax item IDs; SOAP enriches with tax type references, rate percentages, and subsidiary assignments

Every route uses the same authentication: OAuth 1.0 Token-Based Authentication with HMAC-SHA256 signatures computed in memory per request. The signing keys are loaded from credential storage, used to generate the signature, and never written alongside the ERP payload.

A detail worth highlighting: SuiteQL queries dynamically adapt based on each customer's NetSuite edition. A single list_invoices tool maps to up to four query variants depending on whether the account has multi-currency and multi-subsidiary enabled. The proxy detects these capabilities during initial connection setup and selects the correct query shape at runtime - including or excluding JOINs to currency and subsidiary tables as needed.

End-to-End Example: get_invoice (Agent to MCP to SuiteQL to Agent)

Here is the complete request lifecycle when an AI agent calls get_single_netsuite_invoice_by_id. No data touches persistent storage at any point.

Step 1: Agent issues tool call

The agent sends a JSON-RPC 2.0 request to the MCP server:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_single_netsuite_invoice_by_id",
    "arguments": {
      "id": "5831"
    }
  }
}

The MCP server authenticates the request using the token embedded in the URL (a hashed random hex string validated against a key-value lookup), resolves the linked integrated account, and identifies which tool was called.

Step 2: Proxy builds and executes a SuiteQL query

The proxy reads the integration config for the invoices resource with method get, and constructs a SuiteQL query. The query shape adapts based on the customer's NetSuite edition. For an account with multi-currency and multi-subsidiary enabled, the query includes JOINs to both the currency and subsidiary tables:

SELECT
  t.id,
  t.tranid AS invoice_number,
  t.trandate AS issue_date,
  t.duedate AS due_date,
  t.status,
  t.foreigntotal AS total,
  BUILTIN.DF(t.entity) AS contact_name,
  tl.item,
  tl.amount AS line_amount,
  tl.memo AS line_description,
  c.symbol AS currency_code,
  s.name AS subsidiary_name
FROM transaction t
  JOIN transactionline tl ON t.id = tl.transaction
  LEFT JOIN currency c ON t.currency = c.id
  LEFT JOIN subsidiary s ON t.subsidiary = s.id
WHERE t.id = 5831
  AND t.type = 'CustInvc'
  AND tl.mainline = 'F'

This query is sent as a POST to NetSuite's SuiteQL endpoint:

POST /services/rest/query/v1/suiteql HTTP/1.1
Host: {account-id}.suitetalk.api.netsuite.com
Authorization: OAuth oauth_consumer_key="...",
  oauth_token="...",
  oauth_signature_method="HMAC-SHA256",
  oauth_timestamp="...",
  oauth_nonce="...",
  oauth_signature="..."
Content-Type: application/json
Prefer: transient
 
{"q": "SELECT t.id, t.tranid AS invoice_number, ... WHERE t.id = 5831 AND t.type = 'CustInvc' AND tl.mainline = 'F'"}

The OAuth 1.0 signature is computed in memory from the account's stored credentials and discarded after the request is sent.

Step 3: Transform response in memory

NetSuite returns a multi-row result - one row per line item. The proxy evaluates a declarative JSONata expression against the raw response to assemble a single unified invoice object with a line_items array. NetSuite's single-character status codes are mapped to human-readable values (A to OPEN, B to PAID, C to CANCELLED). The raw NetSuite payload exists only in volatile memory during this transformation.

Step 4: Return MCP response

The proxy wraps the transformed result in a standard MCP JSON-RPC response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"result\":{\"id\":\"5831\",\"invoice_number\":\"INV-1042\",\"invoice_type\":\"invoice\",\"status\":\"OPEN\",\"issue_date\":\"2025-03-15\",\"due_date\":\"2025-04-14\",\"total\":12500.00,\"currency\":\"USD\",\"contact\":{\"id\":\"302\",\"name\":\"Acme Corp\"},\"line_items\":[{\"item_id\":\"118\",\"description\":\"Consulting Services - March\",\"amount\":10000.00},{\"item_id\":\"205\",\"description\":\"Travel Expenses\",\"amount\":2500.00}]},\"next_cursor\":null,\"request_id\":\"req_abc123\"}"
    }]
  }
}

At this point, the raw NetSuite response has been garbage-collected. The only place the invoice data exists is in the agent's context window.

Suitelet Passthrough: Downloading a Purchase Order PDF

Some operations cannot be served by SuiteQL or the REST Record API. Generating a Purchase Order PDF requires a custom SuiteScript Suitelet deployed into the customer's NetSuite account, because the REST API has no PDF rendering capability. The Suitelet uses NetSuite's server-side N/render module to call render.transaction() and produce the PDF.

Here is how the passthrough works without caching:

Step 1: Agent calls the download tool

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "netsuite_purchase_orders_download",
    "arguments": {
      "id": "7744"
    }
  }
}

Step 2: Proxy routes to the Suitelet

The proxy reads the Suitelet URL from the connection's context variables (configured during initial integration setup) and makes a request to the customer's RESTlet endpoint:

GET /app/site/hosting/restlet.nl?script={script_id}&deploy={deploy_id}&entity=purchase_order_download&id=7744 HTTP/1.1
Host: {account-id}.restlets.api.netsuite.com
Authorization: OAuth oauth_consumer_key="...",
  oauth_token="...",
  oauth_signature_method="HMAC-SHA256",
  oauth_timestamp="...",
  oauth_nonce="...",
  oauth_signature="..."

The Suitelet executes render.transaction() against PO #7744 inside the customer's NetSuite environment and returns the binary PDF. The proxy streams this response back to the caller without writing it to disk or any persistent store.

Step 3: Binary response returned to agent

The MCP response contains the PDF data. The proxy never saves, caches, or logs the PDF content. The binary payload exists in memory only for the duration of the HTTP round-trip.

This same passthrough pattern applies to fetching dynamic form field metadata. The Suitelet creates an in-memory record via record.create(), introspects its fields (visibility, select options, mandatory state depending on which form and default values were passed), and returns the metadata. The proxy forwards it without persistence.

Validation: Proving No Payloads Hit Persistent Storage

Building a ZDR architecture is one thing. Proving it to an enterprise buyer's InfoSec team is another. Here are the engineering practices that make zero data retention verifiable:

1. Structured logging without payload bodies

Operational logs capture request metadata - timestamps, HTTP status codes, latency, resource type, method - but never the response body. A well-designed log entry looks like this:

{
  "timestamp": "2025-07-15T14:32:01Z",
  "integrated_account_id": "ia_abc123",
  "resource": "invoices",
  "method": "get",
  "upstream_status": 200,
  "latency_ms": 342,
  "request_id": "req_xyz789"
}

No response_body field. No payload field. The absence is the point.

2. Credential isolation from data paths

OAuth tokens and API keys live in secure credential storage, separate from the request processing pipeline. The proxy loads credentials to sign a request, then discards the signing context. ERP payloads flow through a different memory path than credential material - at no point are credentials and business data co-located in persistent storage.

3. No disk writes in the request pipeline

The execution engine processes requests entirely in memory. There are no temporary files, no disk-backed buffers, no swap-to-disk behavior. If the process terminates mid-request, the in-flight data vanishes with it - which is exactly the desired behavior.

4. Audit trail via metadata, not payloads

For SOC 2 and similar compliance frameworks, you need an audit trail showing who accessed what and when. A ZDR architecture achieves this by logging the shape of the access (which resource, which method, which account) without logging the content. This gives auditors the traceability they need without creating a secondary copy of the customer's financial data.

5. Short-lived MCP server tokens

MCP servers can be created with an explicit expiration time, automatically revoking access after a defined window. When the token expires, the authentication material is cleaned up automatically - no stale access credentials linger in the system. This limits the blast radius of a compromised token and aligns with the principle of least privilege. For example, you might give a contractor's agent MCP access for a week, or generate a short-lived server for an automated workflow that runs nightly.

Handling Enterprise API Rate Limits Without Caching

The most common objection to pass-through architecture is rate limits. ERPs like NetSuite and SAP enforce strict request quotas—NetSuite's SuiteQL endpoint, for instance, has concurrency limits that vary by account tier. If your AI agent is making tool calls in a reasoning loop, it can burn through rate limit windows quickly. The instinctive solution is to cache responses locally to reduce upstream API calls.

Resist that instinct. Caching ERP data to avoid rate limits trades a performance problem for a compliance problem. There are better approaches.

There is a dangerous design pattern prevalent in integration middleware: the platform attempts to automatically retry or apply exponential backoff when a third-party API returns a rate limit error (HTTP 429).

If you are building background synchronization jobs, auto-retries are helpful. If you are building AI agents, auto-retries are catastrophic.

When an LLM issues a tool call, it expects a response within a specific temporal window. If your integration middleware quietly absorbs a 429 error and initiates a 60-second exponential backoff, the LLM will timeout, hallucinate a response, or crash the agentic loop entirely. The agent needs to know immediately that the resource is rate-limited so it can decide how to proceed—whether to switch tools, alert the user, or pause its own execution.

The Problem: Every ERP Reports Rate Limits Differently

A major interoperability issue in throttling is the lack of standard headers, because each implementation associates different semantics to the same header field names. NetSuite returns rate limit data in custom headers. SAP uses different conventions. Salesforce has yet another format. If your agent needs to respect rate limits across multiple ERPs, it has to understand each vendor's specific header semantics.

The IETF has been working to fix this. The IETF draft defines RateLimit-Limit (the requests quota in the time window), RateLimit-Remaining (the remaining requests quota in the current window), and RateLimit-Reset (the time remaining in the current window, specified in seconds).

Normalizing Rate Limits Without Absorbing Them

To solve this, a proper ZDR architecture does not retry, throttle, or apply backoff on rate limit errors. When an upstream API like SAP or NetSuite returns a rate-limit error, the proxy layer passes that error directly back to the caller.

What the proxy layer does do is normalize the chaotic, provider-specific rate limit headers into the standardized IETF RateLimit specification. Regardless of whether Salesforce sends Sforce-Limit-Info or HubSpot sends X-HubSpot-RateLimit-Remaining, the ZDR proxy translates these into consistent response headers:

Header Meaning
ratelimit-limit Maximum requests allowed in the current window
ratelimit-remaining Requests remaining before the limit is hit
ratelimit-reset Seconds until the rate limit window resets
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
ratelimit-limit: 100
ratelimit-remaining: 0
ratelimit-reset: 45
 
{
  "error": "rate_limit_exceeded",
  "message": "Upstream provider rate limit reached. Window resets in 45 seconds."
}

By passing these standardized headers back, the caller or AI agent is fully empowered to implement its own retry and backoff logic. The agent retains complete control over its execution state, rather than being held hostage by opaque middleware delays.

// Example: Agent-side rate limit handling using normalized headers
async function callWithBackoff(url: string, options: RequestInit) {
  const response = await fetch(url, options);
 
  if (response.status === 429) {
    const resetSeconds = parseInt(
      response.headers.get('ratelimit-reset') || '60',
      10
    );
    console.log(`Rate limited. Retrying in ${resetSeconds}s`);
    await sleep(resetSeconds * 1000);
    return callWithBackoff(url, options);
  }
 
  // Proactive throttling: slow down before hitting the limit
  const remaining = parseInt(
    response.headers.get('ratelimit-remaining') || '100',
    10
  );
  if (remaining < 5) {
    await sleep(1000); // Gentle slowdown
  }
 
  return response;
}

This design is intentional. Transparency about rate limits—giving the caller the raw information and letting it decide how to respond—produces more reliable agent behavior.

Read our complete guide on best practices for handling API rate limits.

The Technical Blueprint: Declarative Mapping Instead of Code-Per-Integration

The engineering cost of ERP integration is not making the HTTP call. It is translating between the unified schema your application expects and the wildly different data formats each ERP uses. NetSuite represents a contact as a vendor or customer record with PascalCase fields. SAP uses ABAP-style naming. QuickBooks has its own conventions.

Building a ZDR proxy for 100+ integrations usually results in massive code bloat. Most platforms maintain separate code paths for each integration—heavy if/else blocks handling Salesforce, HubSpot, NetSuite, and SAP independently. Adding a new integration means writing new custom translation code, deploying it, and risking regressions across the entire codebase. This is the definition of technical debt that scales linearly.

The most scalable way to build a ZDR architecture is to eliminate integration-specific code entirely. Instead of writing endpoint handler functions for every ERP, you build a generic execution engine that takes a declarative configuration describing how to talk to the API, and a declarative mapping describing how to translate the data. Both are stored as simple JSON blobs in the database.

How It Works: Data Instead of Code

In a declarative architecture, every integration is defined by two pieces of configuration:

  1. Integration Config: A JSON object describing the API surface: base URL, authentication scheme, available endpoints, pagination strategy, rate limiting behavior.
  2. Integration Mapping: Declarative expressions that describe how to translate between the unified schema and the provider's native format.

The runtime engine is a generic execution pipeline that reads this configuration and processes it without any awareness of which integration it is running. The same code path that handles a NetSuite invoice listing also handles QuickBooks, Xero, and every other accounting integration. No if (provider === 'netsuite') conditionals anywhere.

flowchart LR
    A["Unified API Request<br>(e.g. GET /invoices)"] --> B["Generic Engine<br>(one code path)"]
    B --> C{"Configuration<br>Lookup"}
    C --> D["Integration Config<br>(JSON - how to call the API)"]
    C --> E["Integration Mapping<br>(JSONata - how to transform data)"]
    D --> F["HTTP Call to<br>NetSuite / SAP / Xero"]
    E --> G["Response mapped to<br>unified schema"]
    F --> G
    G --> H["Unified Response<br>to Caller"]

JSONata as the Universal Transformation Engine

To transform payloads in memory without writing custom code, you need a functional, side-effect-free transformation language. JSONata is a Turing-complete expression language purpose-built for reshaping JSON objects.

Every field mapping, query translation, and conditional logic rule is stored as a JSONata expression. When a request comes in, the generic pipeline fetches the configuration, evaluates the JSONata expression against the raw ERP payload, and returns the normalized result.

Here is a conceptual example of how a JSONata expression maps a complex, nested ERP employee record into a clean, unified schema suitable for an AI agent:

{
  "response_mapping": "( \n  $resource := $split(body.type,'.')[0];\n  $action := $split(body.type,'.')[1];\n  $event_type := $mapValues($action,{\n      \"created\": \"created\",\n      \"updated\": \"updated\",\n      \"deleted\": \"deleted\"\n  });\n  {\n    \"unified_id\": data.internalId,\n    \"first_name\": data.firstName,\n    \"last_name\": data.lastName,\n    \"department\": data.department.name,\n    \"status\": data.isActive ? 'active' : 'inactive'\n  }\n)"
}

This architecture has a direct benefit for zero data retention: because the engine is a stateless pipeline that evaluates transformation expressions against in-flight data, there is no structural reason for the data to be persisted. The expressions execute against the response payload in memory, produce the transformed output, and the original payload is discarded.

Per-Customer Customization Without Code

Enterprise ERP instances are never generic. Every NetSuite account has custom fields, custom forms, and custom record types. A mapping that works for one customer's NetSuite instance will not work for another's without modifications.

A robust ZDR proxy handles this through a three-level override hierarchy: platform-level defaults, environment-level overrides, and individual account-level overrides. Each level deep-merges on top of the previous one.

Because the mappings are just text strings stored in a database column, they can be versioned, overridden per customer, and hot-swapped without restarting the application. If a specific enterprise customer has heavily customized their NetSuite instance with proprietary fields, you do not need to deploy new code to support them. You simply apply an account-level override to the JSONata mapping configuration. The generic execution engine reads the override at runtime and maps the custom fields dynamically in memory.

Future-Proofing Your AI Agent Integrations

The convergence of AI agents and enterprise ERP systems is accelerating. Oracle NetSuite laid the groundwork for an AI-native ERP experience, with upcoming features expected to embed AI directly into financial and operational workflows. At Microsoft Build, the Dynamics 365 ERP MCP server was introduced as a foundational step in connecting AI and enterprise resource planning systems through a shared, governed protocol.

The direction is clear: ERP vendors are opening their platforms to external AI agents through governed interfaces. The SaaS companies that can connect to those interfaces without introducing compliance risk will win enterprise deals faster.

Connecting AI agents to enterprise ERPs is not just a networking problem; it is a compliance and architecture problem. If you rely on legacy sync-and-store integration platforms, you will spend months fighting InfoSec teams, filling out SIG questionnaires, and explaining why your startup needs to retain copies of a hospital's general ledger.

Here is the strategic takeaway for product and engineering leaders:

  • ZDR is a procurement accelerator. By adopting a Zero Data Retention architecture, you bypass the procurement wall entirely. When your integration middleware stores nothing, the InfoSec section of every security questionnaire gets dramatically simpler. You are not defending a data store—you are describing a proxy.
  • Standardized rate limit headers save agent reliability. Giving your AI agent consistent ratelimit-remaining and ratelimit-reset values across every ERP lets you build one backoff strategy instead of dozens of custom implementations.
  • Declarative integration eliminates per-ERP engineering costs. When adding NetSuite support is a configuration change rather than a code project, you can respond to customer ERP requirements in days instead of months.
  • MCP is the interface contract. The agent ecosystem is standardizing on MCP for tool discovery and execution. Building your ERP integrations behind MCP servers means any compatible agent—Claude, GPT, Gemini, or internal models—can use them without additional work.

The companies shipping enterprise AI products in the coming years will be the ones that solved the ERP connectivity problem without creating a compliance problem. Zero data retention is how you get there. This approach allows your engineering team to stop writing integration-specific code and start focusing on what actually matters: building autonomous agents that deliver measurable business value.

FAQ

What is Zero Data Retention (ZDR) architecture for AI agents?
Zero Data Retention (ZDR) architecture is a stateless pass-through design where your integration middleware processes third-party API payloads entirely in memory, never writing business data to persistent storage. The middleware authenticates, transforms, and forwards requests, then discards the payload once the response is delivered.
Why does caching ERP data kill enterprise SaaS deals?
Enterprise InfoSec teams evaluate vendors through structured SIG questionnaires. ERPs contain sensitive financial data, PII, and sometimes PHI. If your middleware stores this data, you inherit massive compliance liabilities (SOC 2, HIPAA, SOX), which dramatically increases review complexity and frequently causes InfoSec to block procurement.
How do AI agents handle API rate limits in a ZDR architecture?
A proper ZDR proxy does not automatically retry requests. Instead, it normalizes upstream rate limits into standardized IETF headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) and passes them back, allowing the AI agent or orchestration layer to manage its own execution state and backoff logic.
Can AI agents connect to NetSuite through MCP servers?
Yes. MCP servers expose curated ERP tools (like list_invoices or create_purchase_order) with explicit schemas and permission boundaries. Platforms with declarative architectures can auto-generate these MCP tools directly from integration configurations, abstracting complex NetSuite APIs like SuiteQL and SOAP.
What is declarative integration mapping for ERPs?
Declarative mapping defines integration-specific behavior as configuration data (JSON configs and transformation expressions like JSONata) rather than custom code. A generic execution engine reads this configuration at runtime, meaning new ERP connectors and custom field overrides are data operations, not code deployments.

More from our Blog