Most hosts give you two states: a file exists, or it is gone. DurableFile has a retention clock instead. Every file has an end date, and every file can have that date pushed back — at the identical URL.
The default: 14 days
An upload with no expiry fields serves for 14 days:
curl -X POST https://durablefile.com/v1/upload \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-F "file=@handoff.csv"
The response tells you exactly when the link stops working:
{
"hash": "…",
"status": "live",
"public_url": "https://f.durablefile.com/f/…/handoff.csv",
"expires_at": "2026-09-06T17:40:57.000Z"
}
There is no option to skip the clock. That is a deliberate product choice: a $1 payment cannot fund unbounded storage for an unbounded time, so the contract states its limit up front instead of quietly changing later.
Choosing your own window
Send one of two fields, never both:
# Relative: stop serving in 72 hours.
curl -X POST "https://durablefile.com/v1/upload?expires_after_hours=72" \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-F "file=@handoff.csv"
# Absolute: stop serving at an exact instant.
curl -X POST "https://durablefile.com/v1/upload?expires_at=2026-12-01T00:00:00Z" \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-F "file=@handoff.csv"
The rules are strict and return exact errors:
expires_after_hoursmust be greater than zero. A computed zero is rejected instead of silently meaning something else, so a bug in your duration math surfaces as an error.- The maximum window is 4,320 hours — 180 days.
expires_ataccepts epoch seconds or an ISO 8601 time, must be in the future, and must be within 180 days.- Sending both fields is a
conflicting_expiryerror. - Sending an explicit
nullis anexpiry_requirederror. Omit the field if you want the default; there is no way to ask for no expiry.
Expiry is evaluated when the public URL is requested, not by a background job. The link stops serving at its deadline even if no scheduled task ever runs. A separate hourly job reclaims the bytes afterward.
Renewing before the clock runs out
Two gestures renew a file, and both keep the identical URL:
# Republish: an empty body gives a fresh 14-day window.
curl -X POST https://durablefile.com/v1/files/$HASH/publish \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-H "Content-Type: application/json" -d '{}'
# Or set the window explicitly, up to 180 days.
curl -X POST https://durablefile.com/v1/files/$HASH/publish \
-H "Authorization: Bearer $DURABLEFILE_KEY" \
-H "Content-Type: application/json" -d '{"expires_after_hours":4320}'
Re-uploading the same bytes does the same thing, which makes an idempotent CI job a renewal mechanism by accident: each run pushes the deadline out. The mechanics of why the URL is stable are covered in the content-addressing article.
A file that expired but has not been purged yet can still be renewed by the same call. After the purge, the bytes are gone and you must upload them again — which produces the same URL anyway.
Hiding a link early
curl -X POST https://durablefile.com/v1/files/$HASH/unpublish \
-H "Authorization: Bearer $DURABLEFILE_KEY"
The public URL returns 404 from that moment. The bytes stay until the
claim's expires_at, and they keep using your quota until then, so a
republish can bring the link back before the deadline.
Deleting immediately
curl -X DELETE https://durablefile.com/v1/files/$HASH \
-H "Authorization: Bearer $DURABLEFILE_KEY"
Delete frees quota immediately instead of waiting for expiry. It removes
your claim, and GET /v1/me reflects the freed bytes right away.
One nuance in a content-addressed world: if another account independently claims the same bytes, the shared object and its URL survive your delete. The stored object disappears only when the last claim is gone.
The contract in one table
| Action | Public URL | Bytes | Your quota |
|---|---|---|---|
| Live claim | serves | stored | used |
| Manual unpublish | 404 | kept until expiry | still used |
| Expiry passes | 404 | purged within the hour | freed automatically |
| Republish or re-upload | serves again, identical URL | kept | used again |
| Delete | 404 unless another account claims it | kept only if claimed elsewhere | freed now |
Two things worth remembering. Files are public while live, so an expiry is not a way to protect something uploaded by mistake — delete it. And anyone who downloaded a file while it was live keeps their copy; the expiry controls future downloads only.