Skip to content

How to Standardize ATS API Responses for LLMs: The Envelope Pattern Guide

Stop dumping raw ATS API payloads into LLM context windows. Learn how to design standardized JSON envelopes to cut token costs and reduce hallucinations.

Yuvraj Muley Yuvraj Muley · · 13 min read
How to Standardize ATS API Responses for LLMs: The Envelope Pattern Guide

When building AI agents that interact with Applicant Tracking Systems (ATS), dumping raw API responses directly into an LLM context window is an architectural mistake. If you are piping Greenhouse, Lever, or Workable data into an AI agent, the raw JSON you get back from those APIs is actively hostile to LLMs. Deeply nested candidate objects, provider-specific enum values, null-heavy payloads, and inconsistent pagination shapes waste tokens, blow up context windows, and cause hallucinations that look like features until they leak into production.

To fix this, engineering teams must standardize upstream ATS data into a unified schema and wrap it in a lightweight, LLM-optimized JSON envelope. This envelope acts as a translation layer between the chaotic reality of B2B SaaS APIs and the deterministic formatting required by modern AI agents.

This guide breaks down the exact architectural patterns, token economics, and code examples required to standardize ATS responses for LLM consumption. You'll get concrete JSON examples, an architecture pattern that survives multiple providers, and a discussion of how to pass errors and rate limit signals to the agent without breaking its reasoning loop.

The Hidden Cost of Raw ATS Data in LLM Context Windows

Engineering teams often wire an ATS integration to an AI agent by directly proxying the HTTP response. The agent makes a tool call to get_candidate, the backend fetches the data from Greenhouse, and the raw JSON is injected into the prompt. This approach fails at scale for three highly measurable reasons: token bloat, context degradation, and uncontrolled latency.

The Economics of Token Bloat

Raw ATS payloads are massive. A single candidate record fetched from Lever or Greenhouse can easily exceed 4-8 KB of JSON once you include application_ids, educations, employments, attachments, custom_fields, and a nested last_activity block. The payload includes historical application stages, internal system IDs, webhook URLs, empty array fields for unpopulated custom data, and verbose audit logs.

Multiply that by the 50 candidates an agent needs to reason over during a pipeline triage task, and you have burned six figures of tokens before the model has produced a single output. You are paying a premium to process white space, null fields, and "is_active": true flags that have zero relevance to the user's prompt.

Token bloat is not just a cost problem. It's a quality problem. Context window optimization means controlling which tokens enter the LLM context on each request: keeping the relevant ones, removing the rest, and placing critical content where models actually attend to it. When done right, it cuts inference costs by 30-60% and often improves output quality at the same time.

Furthermore, large context windows are disproportionately expensive for API consumers. Google Gemini 1.5 Pro charges twice as much per token for contexts over 128,000 tokens, making long contexts literally more expensive per unit. If your agent loads a full week of Greenhouse activity into a single prompt, you have crossed into the premium pricing tier without gaining any recall benefit.

Context Degradation and the "Lost in the Middle" Phenomenon

LLMs do not parse large JSON objects like a traditional compiler. They rely on attention mechanisms that degrade as the context window expands. The placement issue is more damaging than the cost issue. On multi-document QA with 20 documents, some models lost 20+ percentage points of accuracy when the gold document moved from position 1 to position 10. The key-value retrieval task showed similar patterns: near-perfect accuracy for keys at the boundaries, significant degradation for keys in the middle.

If you ask an AI agent to extract a candidate's required salary or current stage from a raw payload, the model has to maintain attention across thousands of tokens of irrelevant system metadata. When your ATS payload contains 30 unused fields wrapping the one field the agent needs, you are literally burying signal in noise. The actual figure might be buried on line 450 inside a nested custom_fields array. The model is highly likely to hallucinate the answer or simply declare the information missing because its attention was diluted by the surrounding JSON bloat.

Unpredictable Agent Latency

More tokens mean higher Time to First Token (TTFT) and slower overall inference. If your application is an interactive chat interface where recruiters are querying their ATS integrations, asking them to wait 15 seconds while the model digests 500 lines of unoptimized JSON will result in immediate user churn. For a deep dive into the underlying platform fragmentation, start with our ATS integrations architecture guide.

Why Standardizing API Responses is Mandatory for AI Agents

Standardizing API responses for LLM consumption means normalizing provider-specific ATS data into a single, predictable schema before the payload ever reaches the model. This is non-negotiable for AI agents that must reason across multiple ATS platforms.

The ATS market is highly fragmented, and every vendor has a different data model and vocabulary for standard recruiting concepts.

  • Greenhouse separates candidates and applications into distinct endpoints and calls a hiring stage a job_stage.
  • Lever merges the concept of a candidate and an application into a single opportunity object, and calls the stage a stage.
  • Workable structures data around the job itself, nesting candidates under specific job pipelines, using stage_slug.
  • Ashby uses interviewStageId.

If you expose these raw provider schemas to your AI agent, you have to write separate prompt instructions and tool definitions for every single ATS your customers use. Your system prompt becomes an unmaintainable mess of conditional logic: "If the ATS is Lever, look for the 'opportunity' array. If the ATS is Greenhouse, look for the 'applications' array." Function-calling schemas break. Prompt caches invalidate. Structured outputs become unreliable.

The fragmentation is worse than most engineering leaders realize. Consider a single agent workflow: "Find all candidates in a final-round stage across all our recruiting tools, summarize their scorecards, and flag any without an EEOC record." Without normalization, you need:

  • Four different pagination strategies (cursor, offset, next_url, since_id)
  • Four different auth mechanisms (Basic, Bearer, OAuth 2.0, API key in header)
  • Four different enum vocabularies for stage names
  • Four different date formats (ISO 8601, Unix epoch, RFC 2822, mixed)
  • Four different pagination metadata shapes

Even if your engineers can handle that translation layer, the LLM cannot. Every extra shape you expose to the model is another surface area for hallucination. Standardization is not a convenience feature. It is a correctness requirement for autonomous workflows.

You solve this by mapping all provider data to a Unified ATS Data Model (abstracting entities like Candidate, Job, Application, Scorecard, and JobInterviewStage) before the data ever reaches the AI agent. Your agent learns one schema and it works everywhere. For more on this, see our guide on how to integrate multiple ATS platforms.

JSON Token Optimization for AI

Once the data is mapped to a unified schema, you must aggressively compress the JSON payload. Dropping redundant words, removing null or empty values, and keeping keys unambiguous significantly reduces token overhead for LLMs processing structured data. The average LLM API call wastes 40-60% of input tokens on context the model doesn't need.

Consider these envelope design rules that survive production:

  1. Drop nulls at the serialization boundary: If candidate.linkedin_url is null, omit it. Every explicit null costs 4-8 tokens for zero information.
  2. Strip empty arrays and objects: "certifications": [] wastes tokens.
  3. Flatten one-child objects: job: { department: { name: "Engineering" } } becomes job.department: "Engineering". Move deeply nested values up to the root object where possible.
  4. Use short, unambiguous keys: stage beats current_interview_stage_name. Keep them consistent across all entities.
  5. Remove system IDs: Unless the agent needs to make a follow-up PUT/POST request using an ID, strip out internal database UUIDs.
  6. Enumerate stages with a stable vocabulary: Map provider stage names to a normalized set (sourced, screen, onsite_interview, offer, hired, rejected). Include the raw upstream value under meta.provider_stage only if the agent needs it.
  7. Enforce PII Redaction: If the agent's job is triage, it does not need SSNs, DOBs, or immigration status. Ship those behind an explicit tool call. Implement PII Redaction for MCP to ensure private contact details are masked before hitting third-party LLM providers.

Designing the LLM-Facing ATS Response Envelope

The most effective architectural pattern for passing API data to an LLM is the Response Envelope. Instead of returning raw data arrays, you return a structured JSON object that gives the model explicit context about the data it is receiving, instructions on how to interpret it, and the normalized payload itself.

An LLM-facing response envelope is a thin, opinionated wrapper that gives the agent exactly what it needs and nothing else. Three principles drive the design:

  1. Compact. Strip nulls, empty arrays, and redundant IDs.
  2. Position-aware. Put the fields the agent will condition on (status, stage, score) at the top of the payload.
  3. Self-describing. Include a small metadata block so the agent knows what it's looking at and what it can do next.

The Architecture Flow

Here is how the request flows from the AI agent through the orchestration layer to the upstream ATS, and back through the envelope wrapper.

sequenceDiagram
    participant Agent as AI Agent (LLM)
    participant MCP as Tool Server (MCP)
    participant Envelope as Envelope Formatter
    participant UnifiedAPI as Unified ATS API
    participant Upstream as Upstream ATS (Greenhouse, Lever)

    Agent->>MCP: Call tool: get_candidate(email)
    MCP->>Envelope: Request data
    Envelope->>UnifiedAPI: GET /unified/candidates?email=...
    UnifiedAPI->>Upstream: GET /opportunities?email=...
    Upstream-->>UnifiedAPI: Raw Provider JSON (Bloated)
    UnifiedAPI-->>Envelope: Normalized Unified JSON Schema
    Envelope->>Envelope: Strip nulls, flatten, apply Wrapper
    Envelope-->>MCP: LLM-Optimized Envelope JSON
    MCP-->>Agent: Compact, Deterministic Payload

Concrete Envelope Examples

Let's look at the difference between a raw response and an LLM-optimized envelope.

The Bad: Raw Upstream Payload (Truncated for sanity)

{
  "data": [
    {
      "id": "c4f8-4b9a-8a1b-9d8c7e6f5a4b",
      "first_name": "Jane",
      "last_name": "Doe",
      "company": "",
      "title": "",
      "is_private": false,
      "application_ids": [12345, 67890],
      "custom_fields": {
        "salary_expectation": null,
        "start_date": "",
        "willing_to_relocate": true
      },
      "system_metadata": {
        "created_at": "2023-10-01T12:00:00Z",
        "updated_at": "2023-10-05T12:00:00Z",
        "source_id": 999
      }
    }
  ],
  "meta": {
    "skip": 0,
    "limit": 100,
    "total": 1,
    "has_more": false
  }
}

The Good: LLM-Optimized Response Envelope

This envelope provides the model with exact operational context. Notice that identifying metadata comes first, the normalized payload is flat and minimal, and pagination/rate-limit signals are relegated to a control block the agent can ignore during reasoning but use during planning.

{
  "meta": {
    "entity": "application",
    "provider": "greenhouse",
    "unified_schema_version": "2026-01",
    "tool_status": "success",
    "agent_directive": "Analyze the candidate profile. Do not attempt to paginate further."
  },
  "data": {
    "id": "app_01HXY7K3",
    "status": "active",
    "stage": "onsite_interview",
    "candidate": {
      "id": "cand_01HXQ2M9",
      "name": "Jane Doe",
      "headline": "Senior Backend Engineer, 8 yrs",
      "attributes": {
        "willing_to_relocate": true
      }
    },
    "job": {
      "id": "job_9821",
      "title": "Staff Engineer, Platform",
      "department": "Engineering"
    },
    "active_applications": 2,
    "latest_scorecard": {
      "overall": "strong_yes",
      "submitted_at": "2026-08-22"
    }
  },
  "control": {
    "pagination": { "next_cursor": null },
    "rate_limit": {
      "limit": 500,
      "remaining": 487,
      "reset": 1756032000
    }
  }
}

Why this envelope works:

  • meta block: Tells the LLM exactly what happened. The agent_directive field is a powerful pattern. You can dynamically inject instructions based on the API response.
  • Flattened fields: first_name and last_name are combined into name.
  • Stripped empty values: The empty company, title, and null salary_expectation fields are completely removed.
  • Summarized arrays: Instead of passing an array of application IDs that the LLM cannot use without making another network call, we summarize it as "active_applications": 2.
  • Token reduction: This envelope is roughly 400 tokens instead of 2,000, and every field is one the agent actually needs.
Tip

Building for MCP? When serving these envelopes through an MCP server, keep the tool description under 200 tokens and put the envelope schema in the tool's outputSchema block. Agents that see a strict output schema hallucinate significantly less.

Multi-record envelopes for list endpoints

When an agent calls list_applications, do not return an array of full envelopes. Return one envelope wrapping a compact items array:

{
  "meta": { "entity": "application_list", "count": 3 },
  "items": [
    { "id": "app_a", "stage": "screen", "candidate_name": "A. Rivera" },
    { "id": "app_b", "stage": "onsite_interview", "candidate_name": "J. Chen" },
    { "id": "app_c", "stage": "offer", "candidate_name": "K. Patel" }
  ],
  "control": { "pagination": { "next_cursor": "eyJvIjozfQ==" } }
}

The agent gets a scannable summary. If it needs the full envelope for a specific record, it calls get_application(id). This two-step pattern is dramatically cheaper than dumping full records into a list response, as we discuss in our guide on how to feed paginated SaaS API results to AI agents.

Handling Rate Limits and Errors in the Envelope

AI agents are highly prone to infinite loops when they encounter API errors. If an agent calls an endpoint and receives a generic 500 Internal Server Error or a 429 Too Many Requests without context, it will often retry the exact same request indefinitely, burning through your LLM token budget and potentially getting your platform IP banned by the upstream vendor.

Most integration platforms silently swallow 429s and retry in the background, which is fine for cron jobs and catastrophic for LLM agents that reason about their own failure modes. If the agent believes an API call succeeded when it actually retried three times, its plan is now built on a lie.

The Reality of ATS Rate Limits

Upstream ATS platforms have aggressive rate limits. The cleaner pattern is to surface the 429 to the caller and expose rate limit metadata in a standardized shape. Truto follows this model: it does not retry, throttle, or apply backoff on rate limit errors internally. When an upstream API returns an HTTP 429, Truto passes that error directly to the caller, normalizing the upstream rate limit information into standardized headers per the IETF specification:

  • ratelimit-limit: The total number of requests allowed in the current window.
  • ratelimit-remaining: The number of requests left.
  • ratelimit-reset: The time (in seconds or Unix timestamp) until the quota resets.

The caller (your orchestrator or MCP server) is responsible for reading these headers and deciding how to handle the backoff. For AI agents, the best approach is to pass this backoff requirement directly into the LLM envelope.

The Error Envelope Example

Inside the envelope, errors get the same treatment as data. A predictable error shape lets the agent reason about failure without guessing at provider-specific error semantics. If your system hits a 429, you capture the IETF headers and return an envelope that looks like this:

{
  "meta": {
    "entity": "application",
    "provider": "lever",
    "retrieved_at": "2026-08-24T10:14:22Z",
    "tool_status": "rate_limited",
    "agent_directive": "The upstream API is rate limited. You MUST wait 45 seconds before calling this tool again. Inform the user of the delay."
  },
  "error": {
    "code": "rate_limited",
    "http_status": 429,
    "retryable": true,
    "retry_after_seconds": 45,
    "message": "Upstream provider rate limit reached."
  },
  "control": {
    "rate_limit": { "limit": 60, "remaining": 0, "reset": 1756032045 }
  }
}

A few rules that matter here:

  • Explicit agent_directive: You instruct the LLM to pause its execution loop or report the delay to the end user. This prevents the model from hallucinating a response or aggressively retrying the tool call.
  • retryable is a boolean the agent can branch on: Don't force the LLM to parse error strings.
  • retry_after_seconds is derived from the normalized ratelimit-reset header: The agent (or its runtime) schedules its own retry.
  • Auth errors are not retryable: A 401 from an expired refresh token should surface as code: "auth_failed" with retryable: false so the agent stops and hands off to a human or a re-auth flow.

Handling these edge cases is exactly why you need a structured approach to errors. Agents that see well-shaped errors recover gracefully. Agents that see raw provider payloads fail unpredictably. For more on standardizing integration errors, see our guide on 404 Reasons Third-Party APIs Can't Get Their Errors Straight.

Implementing the Architecture with a Unified API

Building this envelope architecture from scratch requires maintaining point-to-point connections with every ATS provider. You have to handle the OAuth token lifecycles, map the custom fields, normalize the pagination logic, and standardize the rate limit headers yourself.

This is where a Unified API drastically reduces engineering overhead. By routing your agent tool calls through the Truto Unified ATS API, the heavy lifting of schema normalization is already done. The platform provides a standardized data model (abstracting Candidates, Jobs, Departments, and Applications) out of the box.

Furthermore, the platform handles the authentication lifecycle transparently. Truto refreshes OAuth tokens shortly before they expire, ensuring your AI agent doesn't fail mid-workflow because a Greenhouse access token died during a long-running candidate sourcing task.

Your engineering team can focus entirely on the LLM-facing logic: stripping nulls, flattening objects, redacting PII, and injecting agent directives. Here is a minimal envelope formatter in TypeScript that sits on top of a unified API:

type Envelope<T> = {
  meta: { entity: string; provider: string; retrieved_at: string; agent_directive?: string };
  data?: T;
  error?: { code: string; http_status: number; retryable: boolean; retry_after_seconds?: number };
  control?: { pagination?: { next_cursor?: string }; rate_limit?: RateLimit };
};
 
function toEnvelope<T>(
  entity: string,
  provider: string,
  raw: Record<string, unknown>,
  headers: Headers
): Envelope<T> {
  const data = compact(raw) as T;
  return {
    meta: { entity, provider, retrieved_at: new Date().toISOString() },
    data,
    control: {
      pagination: { next_cursor: headers.get("x-next-cursor") ?? undefined },
      rate_limit: {
        limit: Number(headers.get("ratelimit-limit")),
        remaining: Number(headers.get("ratelimit-remaining")),
        reset: Number(headers.get("ratelimit-reset")),
      },
    },
  };
}
 
function compact(obj: unknown): unknown {
  if (Array.isArray(obj)) return obj.map(compact).filter((v) => v != null);
  if (obj && typeof obj === "object") {
    return Object.fromEntries(
      Object.entries(obj)
        .map(([k, v]) => [k, compact(v)])
        .filter(([, v]) => v != null && v !== "" && !(Array.isArray(v) && v.length === 0))
    );
  }
  return obj;
}

The trade-off is honest: a unified API is not free. You give up some fidelity in exchange for uniformity, and there will be provider-specific fields (Greenhouse job_post metadata, Lever archive_reason codes) that you'll want to expose through a raw_fields passthrough. That's fine. The envelope pattern supports it. What you gain is an agent that reliably reasons about candidates the same way whether the upstream is Greenhouse, Lever, or Workable.

Where to go from here

When you stop treating AI agents like traditional web frontends and start treating them like highly constrained processing engines, your architecture naturally shifts toward optimization. The envelope is the smallest useful abstraction between messy ATS APIs and an LLM. Standardized schemas, stripped payloads, and explicit instructional envelopes are the difference between an AI feature that looks good in a demo and one that actually works in production.

Get it right and your agent gets cheaper, more accurate, and easier to debug. Get it wrong and you'll be chasing hallucinations across five providers for the next two quarters.

A short checklist to take with you:

  • Normalize schema at the boundary, not in the prompt.
  • Strip nulls and flatten before serialization; every token counts.
  • Put status and stage fields at the top of the payload where the model actually reads.
  • Surface 429s and auth errors to the agent with a retryable boolean; don't hide retries.
  • Expose rate limit info in the IETF standard headers so any orchestration layer can consume it.
  • Keep an escape hatch (raw_fields) for provider-specific data your agent occasionally needs.

FAQ

What is an LLM response envelope for API data?
An LLM response envelope is a lightweight JSON wrapper (typically containing meta, data, error, and control blocks) that normalizes API payloads for AI agent consumption. It strips nulls, flattens nested objects, and places the fields an agent actually reads at the top of the payload to reduce token usage and hallucinations.
Why shouldn't I pass raw ATS API data directly to an LLM?
Raw ATS responses from Greenhouse, Lever, and Workable use different schemas, different pagination shapes, and different enum vocabularies. Feeding these directly to an LLM forces the model to relearn the mapping every call, wastes tokens on null fields, and causes context degradation (the "lost in the middle" phenomenon).
How should an AI agent handle ATS API rate limits?
The agent (or its orchestration layer) should own retry and backoff, not the integration layer. Expose upstream 429 errors directly and surface rate limit metadata via the IETF-standard ratelimit-limit, ratelimit-remaining, and ratelimit-reset headers. Ensure the error envelope includes a retryable boolean and explicit agent directives.
How much can standardizing API responses reduce LLM token costs?
Independent research shows that controlling what enters the context window can reduce inference costs by 30-60% while often improving output quality. For ATS-heavy workflows where raw payloads contain many null or unused fields, the savings can be even higher when combined with flattening and field pruning.

More from our Blog

PII Redaction for MCP: Stop Leaking SaaS Data to LLMs
Security/Guides/AI & Agents

PII Redaction for MCP: Stop Leaking SaaS Data to LLMs

Architectural patterns for redacting PII and standardizing ATS data from Greenhouse, Lever, and Workday before it reaches LLMs via MCP - with code examples, field-level decision matrices, and compliance checklists.

Yuvraj Muley Yuvraj Muley · · 33 min read