Skip to Content

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:

  1. Request an upload URL via createFileUpload. The platform reserves a slot, returns an uploadId and a pre-signed uploadUrl.
  2. PUT the bytes directly to uploadUrl from your client or server.
  3. Confirm the upload via confirmFileUpload. This finalizes the file metadata and returns the canonical File record.

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 } }
ArgumentTypeRequiredDescription
filenameString!yesFilename to store. Used for the canonical URL and as the default display name.
mimeTypeString!yesContent-Type — image/jpeg, video/mp4, application/pdf, etc.
sizeInt!yesSize in bytes. Used to enforce per-plan limits at the gate.
folderStringnoFolder path. Slashes form nesting (campaigns/spring-26). Created on demand.
metadataJSONnoArbitrary 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.jpg
await 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! }
FieldNotes
idStable identifier you store on a record’s image/file field.
filenameCanonical filename. May differ from the upload filename if it was sanitised.
mimeTypeContent-Type detected at upload.
sizeBytes.
urlPublic CDN URL. For image transforms see Media API.
width / heightImage dimensions in pixels. null for non-images.
blurhashA BlurHash  string for placeholder rendering. null for non-images.
altAccessibility text. Editable via foir files update-metadata.
createdAtWhen 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 a FILE_TOO_LARGE error 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 -. Compare filename in the response against what you sent.
  • TTL on uploadUrl: 15 minutes from createFileUpload. After that, request a new URL.
  • Idempotency: confirmFileUpload is idempotent — calling it twice for the same uploadId returns the same File.
  • 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).
Last updated on