Dani Reyes11 min read4 views

The Files API beta header is a response shape, not a flag

The Files API left beta, but the old header did not become inert. It now selects which response shape you get, and the one field it hides is the one that tells you a file has stopped working.

Flat schematic on deep navy: two stacked response outlines for the same file object, the upper one missing a field slot that the lower one fills with a lime marker, showing one header selecting which shape is returned.
Flat schematic on deep navy: two stacked response outlines for the same file object, the upper one missing a field slot that the lower one fills with a lime marker, showing one header selecting which shape is returned.
On this page

Quick answer

As of September 24, 2026, anthropic-beta: files-api-2025-04-14 is no longer a feature flag on the Claude API Files API. The feature is GA and the header is optional. What the header still does is select which response shape you get back, across four separate fields, and it keeps doing that silently for as long as you send it.

That matters most for one field. expires_at is not returned at all while the header is on. So an integration that sets a file expiry and still sends the legacy header is structurally unable to see the timestamp that tells it when the file stops working.

Most writing about the Files API, including most of what currently ranks for it, still teaches the header as a required step. This is a field note about what it actually does now.

The moment

A file-backed job in a small internal tool started failing on a Tuesday. Not intermittently. Every run, same point, and the failure came back fast enough that I assumed it was a validation error rather than anything to do with the model.

It was. The file had expired.

The part that cost me the afternoon was that nothing in my own tooling could have told me. I had a /v1/files listing endpoint wired into an admin page, and the file was still in the list. It had been in the list the whole time. I checked the object for an expiry field, found none, and concluded the file had no expiry set, which is exactly the wrong conclusion and exactly the one the response shape invites.

The file had an expiry. My client was pinned to a shape that does not include it, because two years ago someone pasted in a beta header and it had worked ever since.

Finding 1: The header stopped being required and started being a selector

The Files API documentation is unambiguous about the status change, verbatim:

On the header, verbatim: "The Files API is out of beta and needs no beta header. Migrating off files-api-2025-04-14 is optional: requests that still send it keep working and keep returning the beta response shapes, so an existing integration keeps working until you change it."

Read that carefully, because the reassuring half and the dangerous half are in the same sentence. "Keeps working" is true. "Keeps returning the beta response shapes" is the part that does not feel like a warning and is.

A beta header that has been superseded normally becomes inert. This one does not. It is now a version selector for the response, and the page documents exactly four things it selects:

Scroll to see more

With files-api-2025-04-14Without the header
List responsedata, has_more, first_id, last_iddata, next_page
List cursorsbefore_id, after_idpage, or up to 100 ids[]
expires_at on file objectsNot returnedAlways present, null when unset
Content-Type on the upload partRequiredOptional, detected when omitted

Two of those four are breaking in one direction and one is breaking in the other, which is the detail the rest of this note is about.

Finding 2: The field the header hides is the one that tells you a file stopped working

Row three of that table is the whole story. expires_at is not returned under the legacy shape.

File expiry is a real lifecycle feature. Per the file expiration section of the same page, you set it with an expires_in_seconds form field at upload, an integer between 3,600 seconds and 7,776,000 seconds, and it is set once and cannot be changed afterwards. The resulting timestamp is what tells you the clock is running.

Now put the two together. Nothing stops a client that sends the legacy header from uploading a file with an expiry. The request succeeds. The file expires on schedule. And that client cannot read expires_at on any response, because the shape it asked for does not carry the field.

What happens at expiry is documented, and it is not one behaviour but four:

  • Downloading the content returns a 404
  • A Messages request referencing the file fails before inference runs
  • Its metadata stays readable for up to 30 days, with expires_at in the past
  • It continues to appear in list responses during that window

The documentation's own advice for that last one is to "compare expires_at to the current time to filter expired files", which is sound and is precisely the operation a legacy-shaped client cannot perform.

So the failure mode is not that you get a confusing error. It is that your file list keeps showing you a file that no longer works, and the field that would have explained it was never on the wire. That is what happened to me.

One useful consequence of the pre-inference failure: it is a request-validation failure, not a model failure. It comes back fast and it is not a partial generation you have to reason about.

Finding 3: There are three list shapes, not two

The migration table reads as a binary, header or no header. It is not. A third shape exists, and it is documented in a sentence most readers will never reach, because it sits in the SDK beta namespace note rather than in the migration table.

Verbatim: "Requests that carry anthropic-beta: managed-agents-2026-04-01 without files-api-2025-04-14 receive the shapes on this page with one compatibility affordance on GET /v1/files: before_id and after_id are still accepted (not combinable with page or ids[]), and the list response includes has_more, first_id, and last_id alongside next_page. Later Managed Agents beta versions receive the plain shape."

So the shape of a Files API list response is a function of two unrelated beta headers, one of which is about Managed Agents and has nothing to do with files. The hybrid carries both cursor styles and both pagination field sets at once.

The practical warning: if you are writing the detection logic yourself, do not branch on the presence of has_more. Under the hybrid, has_more and next_page are both present. Branch on next_page, which is present in the new shape and the hybrid and absent in the legacy one.

And note the expiry consequence again. The hybrid gives you the new shapes, so it gives you expires_at. It is specifically the files-api-2025-04-14 header, and only that header, that hides it.

Finding 4: One of the four flips the other way

Three of the four changes make the new shape strictly more informative. The fourth does not, and it is the one that will bite a migration in testing rather than in production.

Content-Type on the uploaded file part is required with the header and optional without it, where the type is detected when omitted.

That is a constraint being relaxed, so removing the header cannot break an upload that was already setting it. But it means the migration is not a pure widening, and it means a test suite that asserts a 400 on a missing Content-Type will start failing for a reason unrelated to anything the suite is about.

The cursor change runs the other way and is a hard break rather than a soft one. Without the header, before_id and after_id return a 400. They do not degrade, they do not get ignored, and they are not silently mapped onto page. If you drop the header and your pagination loop still uses after_id, the loop does not return a short list. It errors.

curl A minimal request under the current shape, with no beta header at all:

bash
curl "https://api.anthropic.com/v1/files?limit=20" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01"

JSON And the field that decides whether you are on the shape you think you are on:

json
{
  "id": "file_011CNha8iCJcU1wXNR6q4V8w",
  "type": "file",
  "filename": "document.pdf",
  "mime_type": "application/pdf",
  "size_bytes": 1024000,
  "created_at": "2025-01-01T00:00:00Z",
  "downloadable": false,
  "expires_at": null
}

If expires_at is absent from that object rather than null, you are on the legacy shape. Absent and null mean completely different things here, and a client that reads them with the same code path will treat "I cannot see the expiry" as "there is no expiry".

Finding 5: The SDK sends the header based on its version, not on what you asked for

This is the one I would have got wrong on my own, because it breaks the assumption that a betas argument is what puts a beta header on the wire.

Verbatim: "Starting with Python SDK 1.2.0, TypeScript SDK 0.122.0, Go SDK 1.68.0, Java SDK 2.59.0, Ruby SDK 1.67.0, and C# SDK 12.44.0, client.beta.files no longer sends files-api-2025-04-14 and returns the same shapes as client.files."

And, on the releases before those, verbatim: "Earlier releases send it from client.beta.files even with no betas argument."

Python So on an older SDK, this call puts a beta header on the wire, and you did not ask for one:

python
uploaded = client.beta.files.upload(file=("report.pdf", data, "application/pdf"))

The namespace you reached for decides the wire format, and the version floor decides whether the namespace does that. Two clients running identical application code against identical arguments can be on two different response shapes because one of them has an older lockfile. Your code is not the thing that determines this.

The fix is the boring one: call client.files. The migration note says so directly, and on the newer releases client.beta.files is equivalent anyway, retained for Files features still genuinely in beta.

This is the same shape of problem as a client library and an API disagreeing about what is valid, which I ran into from the other direction when a Python SDK raised on a schema the API still accepted. There the client was stricter than the wire. Here the client is quietly older than the wire, which is harder to see because nothing raises.

Finding 6: Two smaller things worth knowing before you migrate

Both are in the same documentation and neither is about the header, but both change how you write the client.

ids[] fails silently. You can pass up to 100 file IDs to skip paging. Verbatim: "any ID that does not resolve to a file in your workspace is silently omitted from data; compare the returned IDs against the requested IDs to detect misses." No error, no null entry, just a shorter array. It also cannot be combined with page or limit. If you use it as a batch existence check, you have to do the set difference yourself.

Your own uploads are not downloadable. The downloadable field is false for files you upload. Only files produced by skills or the code execution tool can be fetched back. The Files API is not object storage with a model attached, and a round trip through it will not return your bytes.

While reading the same page I also noted, and did not test, that files Claude generates through the code execution tool carry signed C2PA Content Credentials on download.

Where this leaves the mental model

The thing I had wrong was treating a beta header as a switch with an on state and an off state, where off is the absence of a feature. For this header, off is not the absence of anything. Off is a different, newer, more informative response, and on is a frozen snapshot of an older one.

A superseded beta header that is still accepted is not dead weight. It is a version pin you are carrying without a version number, and the only way to find out which side of it you are on is to look at whether a field is missing.

If you touch one thing after reading this, make it the check in Finding 4: look for expires_at on a file object and see whether it is null or simply not there.

What I did not verify

I did not run any of this against a live API key. Every claim above is read from Anthropic's published documentation on September 24, 2026 and is quoted or cited rather than reproduced from a test run, so treat the behaviours as documented rather than as measured.

I did not test the Managed Agents hybrid shape. The three-shape claim in Finding 3 is a reading of one documented sentence, not something I have seen on the wire, and I have not checked which Managed Agents beta versions count as "later".

I did not verify the per-language SDK version floors by installing each SDK. I did not test whether expires_in_seconds behaves identically when sent alongside the legacy header, which is the obvious next question from Finding 2 and one the documentation does not directly answer.

I have not checked the platform availability matrix beyond noting that the page lists the Files API differently across Claude API, Claude Platform on AWS, Bedrock, Google Cloud and Foundry. If you are not on the first-party API, check that table before assuming any of this applies.

Token accounting for file-backed requests is out of scope here. PromptAttic has recipes for the token counting endpoint that cover that side properly, and I deliberately did not duplicate it.

Postscript: the header had been in that codebase longer than the feature had been in beta, which is its own small lesson about paste.

D

Written by

Dani Reyes

Frequently asked questions

Is the anthropic-beta files-api-2025-04-14 header still required?

No. Anthropic's Files API documentation states that the Files API is out of beta and needs no beta header, and that migrating off files-api-2025-04-14 is optional. Requests that still send it keep working. The header is no longer a feature flag, but it is not inert either: it selects the older response shape for list pagination, expires_at and the upload Content-Type requirement.

Why is expires_at missing from my Claude Files API response?

Because the request is still sending the files-api-2025-04-14 beta header. The documentation's migration table states that expires_at is not returned under the beta shape and is always present without the header, where null means the file has no expiration. A file can have an expiry set and the legacy shape will not show it, so absent and null mean different things and should not share a code path.

What happens when a Claude Files API file expires?

Four things, per Anthropic's documentation as of September 2026. Downloading the content returns a 404. A Messages request that references the file fails before inference runs. The file's metadata stays readable for up to 30 days with expires_at in the past. And it continues to appear in list responses during that window, so an expired file still shows up in a file listing.

Can I still use before_id and after_id to page through Claude files?

Only while you send the files-api-2025-04-14 header. Without it, the documentation states that before_id and after_id return a 400 error and pagination moves to a page and next_page cursor, or up to 100 ids[] values. This is a hard break rather than a silent degradation, so a pagination loop that still uses after_id will error rather than return a short list.

Does the Anthropic SDK send the Files API beta header automatically?

It depends on the SDK version, not on your betas argument. Anthropic documents that from Python SDK 1.2.0, TypeScript 0.122.0, Go 1.68.0, Java 2.59.0, Ruby 1.67.0 and C# 12.44.0, client.beta.files no longer sends the header, while earlier releases send it from client.beta.files even with no betas argument. Calling client.files avoids the ambiguity.

Claude rate limit headers measure two things two ways

The anthropic-ratelimit headers are not one meter. Input is an estimate that gets corrected, output is a running tally, and the tokens header reports whichever limit currently binds.

12 min read25

Your JSON schema is a second cached artifact

Structured outputs and strict tool use turn your JSON schema into a separate cached artifact. It has its own 24 hour lifetime, invalidation rules that invert what you would guess, and a retention boundary that zero data retention does not cover.

10 min read23

Context engineering is four mechanisms, not one

Context engineering on the Claude platform is not a setting. It is four mechanisms at four points in the pipeline, and the one everybody enables first pays for context window with prompt cache.

14 min read23