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.
curl $URL/api/v1/project -H "Authorization: Bearer fh_…"Errors are always the same shape. Match on code, never the message.
{ "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.
# 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_…/completeStep 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#
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#
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_migrationsThe manifest is the export: it is what lets you leave. Nothing about it needs our permission.