Files API
The GraphQL Files API covers uploading and querying media library files — images, video, PDFs, anything you’d reference from a record’s image or file field. Uploads use a two-step “request URL, then upload directly to storage” flow so binary bytes never touch the GraphQL endpoint.
For the broader media-library overview see Media & Files.
Uploading
Uploads are a two-step flow:
- Request an upload URL via
createFileUpload. The platform reserves a slot, returns anuploadIdand a pre-signeduploadUrl. - PUT the bytes directly to
uploadUrlfrom your client or server. - Confirm the upload via
confirmFileUpload. This finalizes the file metadata and returns the canonicalFilerecord.
Both mutations require the files:write scope.
Step 1 — createFileUpload
mutation StartUpload {
createFileUpload(
filename: "hero.jpg"
mimeType: "image/jpeg"
size: 482910
folder: "campaigns/spring-26"
metadata: { alt: "Field of barley at sunrise", source: "drew@bobscountrybunker.com" }
) {
uploadId
uploadUrl
}
}| Argument | Type | Required | Description |
|---|---|---|---|
filename | String! | yes | Filename to store. Used for the canonical URL and as the default display name. |
mimeType | String! | yes | Content-Type — image/jpeg, video/mp4, application/pdf, etc. |
size | Int! | yes | Size in bytes. Used to enforce per-plan limits at the gate. |
folder | String | no | Folder path. Slashes form nesting (campaigns/spring-26). Created on demand. |
metadata | JSON | no | Arbitrary key-value blob attached to the file (alt text, source, copyright, etc.). |
Response:
{
"uploadId": "upl_abc123",
"uploadUrl": "https://storage.foir.io/upload?signature=..."
}The uploadUrl is a single-use, short-lived (15 minutes) pre-signed URL. Treat it like a one-shot capability — don’t log or persist it.
Step 2 — PUT to uploadUrl
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/jpeg" \
--data-binary @hero.jpgawait fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: fileBlob,
});The storage service verifies the signature, the content length, and the content type against what createFileUpload declared. Mismatches reject with 400.
Step 3 — confirmFileUpload
mutation FinalizeUpload {
confirmFileUpload(uploadId: "upl_abc123") {
id
filename
mimeType
size
url
width
height
blurhash
alt
createdAt
}
}Returns the canonical File record. The file is now live in the media library and referenceable from any record’s image/file field by its id.
For images, width, height, and blurhash are populated synchronously during confirmation — your storefront can show a real placeholder immediately rather than wait for a background job.
Reading
Fetch a file by ID
query GetFile {
file(id: "file_abc123") {
id
filename
mimeType
size
url
width
height
blurhash
alt
createdAt
}
}Requires files:read (granted to most read scopes automatically).
The File type
type File {
id: String!
filename: String!
mimeType: String
size: Int
url: String
width: Int
height: Int
blurhash: String
alt: String
createdAt: DateTime!
}| Field | Notes |
|---|---|
id | Stable identifier you store on a record’s image/file field. |
filename | Canonical filename. May differ from the upload filename if it was sanitised. |
mimeType | Content-Type detected at upload. |
size | Bytes. |
url | Public CDN URL. For image transforms see Media API. |
width / height | Image dimensions in pixels. null for non-images. |
blurhash | A BlurHash string for placeholder rendering. null for non-images. |
alt | Accessibility text. Editable via foir files update-metadata. |
createdAt | When the file was first confirmed. |
Browser-direct uploads
Because uploadUrl is just a signed URL, you can hand it straight to a browser without touching your server:
// Server: request the upload URL with your secret key
const { uploadId, uploadUrl } = await foir.createFileUpload({ ... });
// Client: PUT the bytes directly to storage
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});
// Server: confirm to get the canonical File record
const created = await foir.confirmFileUpload({ uploadId });This keeps large file bytes off your server entirely — your only job is brokering the URL and confirming the upload.
Limits & validation
- Size: enforced against your billing plan’s per-file limit at
createFileUpload. Oversize attempts reject with aFILE_TOO_LARGEerror before any bytes are uploaded. - MIME type: must be declared up front. The storage service checks the actual content-type of the bytes against the declared one and rejects mismatches.
- Filename: sanitised on upload — unsafe characters replaced with
-. Comparefilenamein the response against what you sent. - TTL on
uploadUrl: 15 minutes fromcreateFileUpload. After that, request a new URL. - Idempotency:
confirmFileUploadis idempotent — calling it twice for the sameuploadIdreturns the sameFile.
Related
- Media & Files — feature overview.
- Media API — image transforms (resize, crop, format conversion) applied to
File.url. foir media upload— CLI wrapper that hides the three-step dance behind a single command.foir files— CLI for managing files after upload (rename, tag, move, delete).