AI agent data minimization: control what your APIs hand to the model
AI agent data minimization belongs at the API gateway, not in the prompt. Allowlist response fields per agent, keep PII out of model context, and prove it.
- ai-agents
- mcp
- security
- data-minimization
- workflows
- compliance
Most AI agent security reviews we sit in focus on what the agent is allowed to call. Far fewer ask what the agent receives when it calls. That gap is where AI agent data minimization lives. When a support agent calls GET /customers/{id} to check whether a card is blocked, the backend usually returns the whole customer record: date of birth, full IBANs, national ID, home address, risk flags. All of it lands in the model's context window, may be repeated into the next tool call, and can end up in the LLM provider's logs or your own conversation store.
The access decision was correct. The agent was entitled to look up that customer. The data decision never happened, because nobody built one. For a CISO or a data protection officer, this is the uncomfortable part: least privilege at the endpoint level says nothing about least privilege at the field level, and agents consume fields.
This post covers why the usual fixes do not close that gap, and how to enforce per-agent response minimization at the gateway in Zerq, with the workflow configuration, the MCP call the agent makes, and the request log entry that proves what the agent saw.
Why prompt rules and DLP scanners do not minimize data
The first instinct is to tell the model not to repeat sensitive fields. That is a behavior request, not a control. The data has already crossed into the model's context by the time the instruction applies, and a prompt injection in any other tool result can override it. If the record contained a national ID, the model has seen a national ID.
The second instinct is a DLP layer on the LLM side: an AI or LLM gateway that scans prompts for patterns like card numbers before they reach the provider. That helps with obvious formats and misses everything else. A customer's risk classification, a medical note, or an internal fraud score has no regex. Pattern scanning is also probabilistic in both directions: false negatives leak, false positives break the agent mid-task.
The third approach is a field filter on the API gateway. Kong's response-transformer plugin can remove JSON keys, Apigee can strip fields with AssignMessage or JavaScript policies, and Azure APIM has set-body policies. These work for the traffic that passes through them. Two architectural problems remain. First, agent traffic often does not pass through them: teams stand up a separate MCP server that calls backends directly with its own service credentials, so the REST filter never sees the agent's requests. Second, these filters are usually written as denylists ("remove ssn and dob"). When the backend team adds passport_number next quarter, the denylist does not know about it and the field ships to every consumer, including the agent, by default.
How Zerq enforces AI agent data minimization at the gateway
Zerq approaches this with three properties that matter for minimization. Agents use the same gateway as every other consumer, through Gateway MCP. The minimization logic runs inside the gateway as a proxy workflow, so it is versioned and audited like any other config change. And the request log stores the body the gateway sent back to the caller, not the body the backend returned, so you can prove what the agent received.
The request path, end to end
Here is the data flow for an agent call, described so you can draw it:
- The AI client (Claude, Cursor, an internal agent framework) sends a JSON-RPC
tools/callforexecute_endpointtoPOST /mcpwithAuthorization,X-Client-IDandX-Profile-IDheaders. - The Gateway MCP handler builds an ordinary gateway HTTP request to the published path, sets
X-Client-ID,X-Profile-IDandX-Gateway-Source: mcp, and sends it through the same gateway pipeline REST clients use. - The gateway auth middleware validates the client, checks that the profile belongs to that client, verifies the profile's credential, then applies the profile's IP allowlist and allowed methods.
- The matched proxy has a workflow enabled. The workflow calls the backend, receives the full record, runs an allowlist transform, and returns only the minimized object.
- The request logger writes one entry with client ID, profile ID, request headers and the response body exactly as sent.
- The MCP handler wraps the gateway response as the tool result. The model only ever sees the minimized body.
The backend never talks to the agent, and no field reaches the model unless the workflow put it there. That is the inversion from a denylist: new backend fields are dropped until someone deliberately adds them.
Step-by-step: build an agent-facing minimized endpoint
The cleanest pattern is a dedicated collection for each agent use case. Discovery in Gateway MCP is scoped by the client's collections, so a support agent client that only has the support-agent collection only discovers those endpoints.
- In the Management UI, create a collection named
support-agentwith base path/support-agent. This is the agent's entire API surface. - Add a proxy with the multi-step proxy form. On Basic Information name it
Get customer summary; on Path Configuration set the path to/customers/{customerId}and the method toGET. - On the Request/Response Schema step, fill in Response Schema and Response Example with the minimized shape, not the backend's full record. The
endpoint_detailsMCP tool returns these to the agent, so they should describe exactly what the agent will get, with synthetic values only. - Open the proxy's workflow canvas. A new workflow starts as
http_trigger -> response_node. Add anhttp_request_node, acode_nodeand a secondresponse_nodeas shown below. - Under Clients, create a client for the agent and assign it only the
support-agentcollection. Click Add Profile, choose an auth type, and set Allowed Methods toGET. A non-GET call from this profile returns405before any workflow runs. Copy the X-Client-ID and X-Profile-ID values from the profile detail page. - Click Validate on the workflow, run it with sample data or the Listen for Request test mode, then Save and toggle Enable Workflow. Publish the proxy. Until the workflow is enabled the proxy falls back to pass-through forwarding, which here means the full record, so check the enabled state before you hand the agent its credentials.
The workflow configuration
The backend call goes in an http_request_node. The trigger hands the workflow the request's method, path, headers, query and body, so the customer ID is taken from the last path segment and encoded before it goes into the URL:
{
"id": "fetch_customer",
"type": "http_request_node",
"data": {
"config": {
"timeout_ms": 3000 // fail fast; a stalled CRM should not stall the agent
},
"inputs": {
"url": "https://crm.core.internal/v2/customers/{{ encodeURIComponent($json['http_trigger'].request.path.split('/').pop()) }}",
"method": "GET", // the agent profile is GET-only anyway
"headers": { "Accept": ["application/json"] }
}
}
}
The node takes its success branch only on a 2xx response. JSON responses are parsed, so the next node reads fields directly from response.body. The minimization itself is a code_node. Code lives in config.code, reads upstream outputs from $json, and returns the new object as result:
{
"id": "minimize",
"type": "code_node",
"data": {
"config": {
"timeout_ms": 500,
"code": "const c = $json['fetch_customer'].response.body || {}; const mask = v => (typeof v === 'string' && v.length >= 4) ? '****' + v.slice(-4) : null; return { customer_id: c.customer_id, first_name: c.first_name, segment: c.segment, kyc_status: c.kyc ? c.kyc.status : null, cards: (c.cards || []).map(k => ({ card_ref: mask(k.pan), status: k.status, blocked_reason: k.blocked_reason || null })), open_cases: c.open_cases_count || 0 };"
}
}
}
Read that code as a contract. Six top-level fields leave the gateway. The card number becomes a four-digit reference the agent can quote back to a customer. Date of birth, address, national ID, full account numbers, the internal risk score and anything the CRM team adds next year are not in the returned object, so they do not exist for the agent.
The closing response_node returns the minimized object:
{
"id": "respond",
"type": "response_node",
"data": {
"inputs": {
"status_code": 200,
"headers": { "Content-Type": ["application/json"] },
"body": "{{ $json['minimize'].result }}" // full expression: the object itself, not a string
}
}
}
Two failure edges need their own response_node, and both must fail closed. Wire the error output of fetch_customer to a response returning status {{ $json['fetch_customer'].response.status_code || 502 }} with a fixed body such as {"error": "customer_lookup_failed"}. Do not pass the backend's error body through: upstream error messages are a common place for record fragments to leak. Wire the error output of minimize to a 500 with {"error": "response_shaping_failed"}. If the transform breaks, the agent gets nothing rather than the raw record.
The same transformation patterns are documented as reusable recipes on the request and response transformation use case page, and the capabilities overview lists the other workflow nodes you can chain in front of the transform.
What the agent sends and receives
With the client and profile in place, the agent's MCP client calls execute_endpoint with a method and a full gateway path:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "execute_endpoint",
"arguments": {
"method": "GET",
"path": "/support-agent/customers/C-20931"
}
}
}
The tool result carries the gateway's status code, response headers and body as a string. This is everything the model sees:
{
"status_code": 200,
"body": "{\"customer_id\":\"C-20931\",\"first_name\":\"Lena\",\"segment\":\"retail\",\"kyc_status\":\"verified\",\"cards\":[{\"card_ref\":\"****4417\",\"status\":\"blocked\",\"blocked_reason\":\"suspected_fraud\"}],\"open_cases\":1}",
"headers": { "Content-Type": ["application/json"], "X-Request-Id": ["9f3c2a71-6b0e-4d52-8e19-c4a7d05b2e66"] }
}
The agent can answer "your card ending 4417 was blocked for suspected fraud and there is one open case" without ever holding the card number, the customer's birth date or their address.
What the request log proves
Every execute_endpoint call creates a request log entry, and the response body field is the body the gateway sent to the caller. An illustrative entry for the call above, using the documented log fields:
{
"request_id": "9f3c2a71-6b0e-4d52-8e19-c4a7d05b2e66",
"timestamp": "2026-10-19T08:42:15Z",
"method": "GET",
"path": "/support-agent/customers/C-20931",
"status_code": 200,
"latency_ms": 64,
"client_id": "6712b0e4c1a94f2d8e3b5a10",
"profile_id": "6712b0e4c1a94f2d8e3b5a11",
"collection": "support-agent",
"request_headers": { "X-Gateway-Source": ["mcp"] },
"response_body": "{\"customer_id\":\"C-20931\",\"first_name\":\"Lena\",\"segment\":\"retail\",\"kyc_status\":\"verified\",\"cards\":[{\"card_ref\":\"****4417\",\"status\":\"blocked\",\"blocked_reason\":\"suspected_fraud\"}],\"open_cases\":1}"
}
For a compliance team, client_id and profile_id identify which agent made the call, and those are authenticated: the gateway verified the profile's credential and that the profile belongs to the client. response_body is the evidence for a data minimization question: it shows exactly which fields left your boundary toward the model. Treat X-Gateway-Source: mcp as a useful label for separating agent traffic in reviews, not as identity, since extra headers in a tool call can overwrite it. Filter by Profile ID when the question is "what did this agent receive", and use the Payload filter to search response bodies for any field name that should never appear, such as date_of_birth. A search that returns nothing for the agent's profile is a check you can rerun every quarter. The observability page covers the dashboards and the audit role that lets reviewers read these logs without admin rights.
Changes to the workflow itself, including someone widening the allowlist, land in the separate audit log with who changed it and when. Minimization is only as strong as its change control, so route allowlist edits through the same review as any other access change. The security page describes the RBAC roles that keep edit and audit duties separate.
A decision checklist for per-agent minimization
Apply these questions to each agent use case before it reaches production:
- Does every endpoint the agent can discover return an explicit allowlist of fields, rather than the backend's record minus a few?
- Can the agent complete its task with masked or derived values (last four digits, a status, a count, a boolean such as
is_over_18) instead of the raw field? - Do the endpoint's Response Schema and Response Example describe the minimized shape with synthetic values, so
endpoint_detailsitself discloses nothing? - Do both failure paths return a fixed error body, so an upstream error message cannot carry record fragments to the model?
- Is the agent's client assigned only its own collection, and is its profile limited to the methods the task needs?
- Is the workflow enabled and the proxy published, with a test confirming a forbidden field is absent from the response?
- Is the gateway response cache off for agent collections? The shipped
deploy/.envsetsGATEWAY_ENABLE_CACHE=true, and the cache middleware answers repeat GETs before auth, workflows and the request logger run, so a cached hit leaves no log entry for that read. - Can your compliance team run a saved request log query for that agent's profile ID and a forbidden field name, and get zero results?
What this looks like in practice
Consider a retail bank rolling out a customer service agent that answers card and case questions. In the first pilot, the agent called the same GET /customers/{id} endpoint the mobile app uses, through a standalone MCP server holding a CRM service account. The data protection officer pulled a sample of conversation transcripts and found dates of birth and full card numbers quoted back by the model, all from tool results the agent never needed. The pilot stopped.
The rebuild moved the agent onto Zerq. The platform team created a support-agent collection with four proxies, each with a workflow returning an allowlisted shape: customer summary, card status, recent declines with merchant name and amount only, and open case list. The agent got its own client with only that collection and a GET-only profile. The standalone MCP server and its CRM service account were removed, so the only route from the agent to customer data was through the gateway.
The data protection officer now runs two saved log views: every call from the agent's profile in the last 30 days, and the same view with a Payload filter for date_of_birth, pan and national_id. The second view has stayed empty. When the CRM team added a passport field to the customer record, nothing changed for the agent, because the allowlist never included it. The mobile app's endpoint kept its full shape, unaffected.
Summary
Per-agent response minimization at the gateway gives you what prompt rules and DLP scanners cannot. The allowlist decides which fields reach the model, so new backend fields stay out by default. Agents and apps share one gateway, so there is no side path where the filter does not apply. Failure paths return fixed errors, so a broken transform or a verbose backend error cannot leak a record. The request log stores the exact body each agent received, tied to an authenticated client and profile, so data minimization becomes something you can show an auditor rather than something you assert. To see how agent access, profiles and audit fit together, start with the AI agents and MCP access use case.
Zerq is an enterprise API gateway built for regulated industries — one platform for API management, AI agent access, compliance audit, and developer portal, running entirely in your own infrastructure. See how it works or request a demo to walk through your specific requirements.