<!-- section: Files API · status: building · source: docs/files-api/07-signed-urls.md -->

> Private files expire after five minutes. How to get a fresh set, and when not to.


# Signed URLs

A public file's URL is permanent. A private file's is signed and valid for five
minutes, which is long enough to render a page and short enough that a leaked
link is worthless by the time it is shared.

## POST /v1/files/{id}/url

```bash Request
curl -X POST "$BASE/v1/files/abc123def456/url" \
  -H "authorization: Bearer $KEY"
```

```json Response
{
  "url": "https://cdn…?Policy=…&Signature=…&Key-Pair-Id=…",
  "variants": [{ "width": 100, "url": "…" }],
  "srcset": "…100w, …300w, …",
  "expiresIn": 300
}
```

`expiresIn` appears only for private files. On a public one the field would be a
lie, so it is absent.

## This has exactly one job

Re-signing a private file after its five minutes are up. Public URLs never
expire and every list response already carries them, so calling this in a loop
over a listing is an N+1 round trip that buys nothing the listing did not
already give you.

## Never persist a signed URL

By the time a cached page renders, it has expired. Read them fresh per request,
and cache the *file id* rather than the link.

## Public URLs are the opposite

Permanent, derived from ids, and safe to store on your own record if you want to
render without calling us at all — a static page, or a feed built ahead of time.

That is an optimisation rather than a requirement. The same call returns the
same strings every time, so putting it behind whatever caching your framework
already has is usually enough, and it costs you nothing to invalidate when a
file is replaced.

The one thing that breaks a cached public URL is
[changing the file's visibility](/docs/files-api/update-files), which moves the
object.

## Errors

```json 404
{ "error": "File not found" }
```
