FileHutch
Documentation

Reference

API reference

One versioned REST API under /api/v1. Everything the dashboard does, the API does, because both call the same services.

Authentication#

A bearer token scoped to one project, from Dashboard → API keys. It is shown once. Read-only keys can call any GET; anything that writes needs a full key.

Terminal

curl $URL/api/v1/project -H "Authorization: Bearer fh_…"

Errors are always the same shape. Match on code, never the message.

JSON

{ "error": { "code": "policy_violation", "message": "image/png is not allowed" } }

Uploading#

Three steps, and the bytes never pass through FileHutch: ask for an upload, PUT to the URL you get back, then say it is done.

HTTP

# 1. against a policy, which decides type, size and visibility
POST /api/v1/uploads
{ "policy": "documents", "filename": "report.pdf",
  "content_type": "application/pdf", "byte_size": 12345,
  "metadata": { "order_id": "ord_1" } }

# 2. straight to storage, with the headers you were given
PUT <upload.url>

# 3. verifies size and records the checksum
POST /api/v1/uploads/file_…/complete

Step 1 takes an optional checksum: the MD5 of the file, with or without an md5: prefix. Send it and step 3 verifies the stored object against it, so a truncated or corrupted upload fails rather than becoming a file. Leave it out and only the byte count is checked. The clients compute it for you.

A public file's url is a FileHutch address — /f/file_abc123, and /f/file_abc123/thumb for a named transform. It redirects to your bucket, and it is the one to store: move the project to another provider and every link you have already saved keeps working, because the bucket and the object key are resolved per request rather than baked into the URL.

Step 1 answers plan_limit when the upload would take the team past its storage allowance. That is a hard stop by default — worth surfacing to whoever is uploading rather than retrying. A team can turn on per-GB overages under Settings → Plan, which lets uploads through until that period's overage charges reach the limit they set; the same code comes back when they do.

Files#

HTTP

GET    /api/v1/files/file_…               # url is present only for public files
POST   /api/v1/files/file_…/signed_url    # { expires_in, disposition }
POST   /api/v1/files/file_…/transform_url # { transform, expires_in }
DELETE /api/v1/files/file_…               # 204; the id then reads as "deleted"

Objects already in a connected bucket become files in place with POST /api/v1/files/adopt or, for a whole prefix, POST /api/v1/storage_imports; see Storage.

transform_url answers plan_limit once the team has used its monthly transform allowance on managed storage; see Transforms.

A file carries id, filename, content_type, byte_size, checksum, visibility, status, metadata, policy, url, transforms and created_at. No storage keys, no provider URLs.

Everything else#

HTTP

GET  /api/v1/project             # storage status, policies, transforms
GET  /api/v1/transforms          # the project's named image sizes
GET  /api/v1/manifest            # every ready file and its object key

POST /api/v1/config/plan         # what applying this config would change
POST /api/v1/config/apply        # make it so
GET  /api/v1/config              # the project as declarative config

GET  /api/v1/webhook_endpoints
POST /api/v1/webhook_endpoints
GET  /api/v1/storage_migrations  # moves, with progress
POST /api/v1/storage_migrations

The manifest is the export: it is what lets you leave. Nothing about it needs our permission.