On May 1, 2026 — today — Exa finished a three-step API deprecation that's been quietly half-breaking AI agents for the past two weeks. The hard cutoff today is the visible piece. The interesting part is what's been broken since April 15 with no error.
Three things changed:
-
/research/v1— sunset today. Calls now fail. Migration:/searchwithtype: "deep-reasoning". -
startCrawlDate/endCrawlDate— silently ignored since April 15. Requests still return200. The filters just don't apply. -
resolvedSearchTypeandhighlightScores— started returningnullon April 15. Hard-removed today.
If you're calling Exa from an agent, RAG pipeline, or research tool, at least one of these is probably touching your code path. The endpoint cutoff is the loud one — your app errors and you find out. The other two are the silent-breakage class. Your code keeps running, your tests stay green, your output gets quietly worse.
The Silent One: Crawl-Date Filters
This is the part most teams will miss.
Exa's /search endpoint takes startCrawlDate and endCrawlDate parameters. They constrain results to pages crawled within a date window — useful when you want recent web content and don't want a 2019 blog post showing up because Google still ranks it.
A typical usage pattern, paraphrased:
results = exa.search(
query="latest LLM benchmarks",
startCrawlDate="2026-04-01T00:00:00Z",
endCrawlDate="2026-05-01T00:00:00Z",
numResults=20,
)
Before April 15, you got pages crawled in April 2026.
After April 15, Exa accepts the request, returns 200, and gives you whatever the search would return without the date filter. The parameters are accepted, validated for type (still ISO 8601), and discarded.
There's no error. There's no warning header. The Exa changelog is the only place this is documented:
"requests will succeed, but the filter will have no effect"
The Exa reference docs still list both parameters in the current request schema, with no deprecation marker. So the integration looks correct in every place a developer would look — except for the runtime behavior.
The class of code that breaks here is "pull recent content for an LLM context window." If you're doing date-bounded research for an agent — "summarize what's been written about X this month" — the agent is now happily summarizing pages from any year. The output reads fine. Spot-checking ten queries you might not notice. The tells are subtle: results gradually skewing older, "this month's news" articles citing 2024 sources, RAG answers that are confidently wrong about when something happened.
The Almost-Silent One: Null Response Fields
Two fields started returning null on April 15:
-
resolvedSearchType— when you call/searchwithtype: "auto", this told you which algorithm Exa actually used (neural, keyword, etc.). Useful for logging, for A/B comparisons, for tuning prompts based on which retrieval path won. -
highlightScores— relevance scores for the highlights extracted from each result. Useful for filtering ("only show highlights above 0.7"), for re-ranking, for explaining results to users.
Code that reads these fields now gets null instead of a number or a string. Whether that breaks loudly depends entirely on how the consumer is written.
// This silently degrades:
const ranked = results.flatMap(r =>
r.highlights.map((h, i) => ({ text: h, score: r.highlightScores[i] }))
).filter(h => h.score > 0.7);
// .filter on undefined > 0.7 → false for every item → empty array
// No error, just no results.
// This crashes loudly:
const avg = results.reduce((a, r) => a + r.highlightScores.reduce(...), 0);
// Cannot read property 'reduce' of null → 500 in your handler.
The crash version gets caught in CI on the next run. The silent-empty version ships and your "top relevant snippets" feature returns nothing forever. Then on May 1, the field is removed entirely, the null becomes undefined, and downstream code that was previously handling null may behave differently again.
This is the same pattern as Stripe's current_period_end move and Auth0's TLS cipher removal: the SDK still validates, the response still parses, the field your code depends on just isn't there.
The Loud One: /research Endpoint
/research/v1 — the agentic research endpoint — is gone today. Calls return an error.
Migration is to /search with type: "deep-reasoning" and an outputSchema for structured output. From the Exa docs, the new shape is:
{
"query": "your research question",
"type": "deep-reasoning",
"outputSchema": { "type": "object", "properties": { ... } }
}
Three things to watch in the migration:
-
Cost shape changes. The new endpoint exposes
costDollarsper request; budget code that hard-coded research-endpoint pricing will be wrong. Pull cost from the response, not from a constant. -
Streaming differs.
/searchsupportsstream: true; if your/researchintegration was synchronous, you can leave streaming off, but it's worth a look — agents tend to feel slow ondeep-reasoningqueries. -
Output schema enforcement is opt-in. With
/research, you wrote schemas and got typed output. With/searchdeep-reasoning, you only get typed output if you passoutputSchema. Forget it and you get free-form text. Consumers that didJSON.parse(result.output)will throw.
This is the visible failure. Loud is preferable to silent.
Why Most Tests Won't Catch the Silent Pieces
Standard ways teams miss this class of change:
Mocked Exa in tests. If your test fixture is a recorded /search response from March, it has highlightScores and resolvedSearchType populated. Your code reads them, your assertions pass. The live API now returns null. Tests are green; production is degraded.
startCrawlDate parameter still in docs. A developer auditing the integration today, looking at Exa's reference page, sees both parameters listed normally. The deprecation notice is on the changelog page only. So a code review of the integration doesn't catch it — the code matches the docs.
Agents don't have unit tests. Most agent loops are tested by running them and eyeballing the output. The output of a date-unfiltered query looks similar enough to a date-filtered one that the difference doesn't jump out, especially if the underlying corpus is dominated by recent pages anyway.
Schema validators pass. If you're using zod or pydantic to validate the response, null likely passes a nullable field schema, and a missing field passes an optional field schema. Validation errors are not the right signal here.
How to Detect This Class of Change
The general defense is the same as for Stripe's Basil migration, GitHub's merge_commit_sha removal, and OpenAI's input_text rejection: you have to watch the shape of the response, not just the status code.
Specific things you can do today:
-
Audit
startCrawlDate/endCrawlDatecallers.grep -r "startCrawlDate\|endCrawlDate"across your codebase. Anywhere you pass these, the filter is now no-op. Decide if you can remove them, replace withstartPublishedDate/endPublishedDate(which still work but mean something different — published date as Exa understands it, not crawl date), or whether your use case requires a different data source. -
Search for
highlightScoresreads. Anywhere your code accesses this, it's nownull(and gone after today). Most consumers want to either drop the score-based logic or replace it with a re-ranker. -
Search for
resolvedSearchTypereads. Mostly used for logging or analytics — usually fine to drop, occasionally drives branching logic that needs replacing. -
Migrate
/researchcallers to/searchwithtype: "deep-reasoning". CheckoutputSchemais set if you parse the output as JSON.
For ongoing monitoring: schedule a daily script that hits your top Exa endpoints with a representative query, hashes the field set in the response, and alerts when the hash changes. That signal catches every future silent removal — not just Exa's.
The Pattern, Now Five Months In
This is the twelfth provider I've written up in this series. The shape varies; the failure mode is consistent:
| Provider | Surface | What Goes Wrong |
|---|---|---|
| Stripe Basil | Subscription.current_period_end |
Moved to items[].current_period_end; old reads return undefined
|
| GitHub | pull_request.merge_commit_sha |
Returns null on closed PRs in API ver 2026-03-10 |
| GitHub | Org security fields | PATCH returns 200, applies nothing |
| OpenAI | Responses input_text
|
Rejected with Invalid value error |
| HubSpot | Contacts v1 endpoints | Return 200 with list-memberships silently dropped |
| Auth0 | TLS handshake | Weak ciphers start returning handshake_failure Jun 10 |
| Twilio | api.de1.twilio.com |
Removed; regional domains never actually routed regionally |
| Shopify | Checkout metafields
|
Returns undefined after 2026-04; orders ship without app data |
| Kubernetes 1.36 |
gitRepo volumes |
Pass validation, fail at deploy time with FailedMount |
| Anthropic | claude-3-haiku-20240307 |
Returns model-retired error after Apr 20 |
| OpenAI | DALL·E 2/3 | Retired May 12; per-image billing flips to per-token |
| Exa | /research + crawl-date filters + highlightScores |
Endpoint 404, parameters silently ignored, fields null |
Twelve providers, twelve different shapes, one shared failure mode: the API still returns 200 (until it doesn't), the SDK still validates, the field your code depends on just isn't there — or the parameter you sent is being thrown away.
If your AI stack pulls from Exa and you haven't audited the calls yet, today is the day. Tomorrow, "I noticed our research agent is summarizing weirdly old pages" is a much harder bug to track down than "I removed the startCrawlDate parameter on May 1 because it stopped working."
I'm building FlareCanary for exactly this problem — point it at the API endpoints you depend on (Exa, OpenAI, Stripe, GitHub, anything), and it polls them on a schedule, learns the response shape, and alerts when a field drops, a type flips, or a parameter starts being ignored. Free tier covers up to five endpoints — useful for keeping watch on your top external dependencies without writing the monitor yourself.
You don't need a tool for this. You do need a habit. The Exa rollout is unusually gentle — the deprecation notice was clear, the changelog was explicit, the migration path was documented. The silent half-broken state still ran for two weeks because nobody had a system for catching "the parameter we send is being thrown away."
That's the gap. HTTP 200 isn't enough. The shape matters.
If your agent or RAG pipeline tripped on Exa's deprecation today — or any other silent schema change — I'd like to hear about it. The "no error, just degraded output" failures are the ones I'm most interested in. Drop a comment or reach out.
Top comments (0)