Skip to content

API reference ​

Every app gets this guide in its dashboard with its own folder, sizes and limits filled in. The values below are the defaults of a new app called your-app. The machine-readable version is openapi.json.

Contents ​

  1. How it works
  2. Authentication
  3. Your configuration
  4. Endpoints
  5. Code examples
  6. Errors
  7. Good practices

1. How it works ​

Your backend sends a file to the API. For an image the service:

  1. checks the file and fixes its rotation,
  2. creates every size configured for your app in WebP,
  3. removes metadata (EXIF, GPS),
  4. stores the files on the CDN under your folder your-app/,
  5. answers with the public CDN URL of every size.

For a document (PDF, Word, Excel, PowerPoint, CSV or text) nothing is re-encoded. The file is stored as it arrived, under the same folder, and you get back one public URL plus a short summary of what is inside it (pages, rows, slides, words).

SVG is the exception on the image side: it is rewritten through a strict allowlist first, so scripts, event handlers, external references and entity declarations never reach storage. What is published is the cleaned vector plus the usual raster sizes, drawn at whatever resolution each size needs.

You store the returned id and URLs in your own database and use the URLs directly in your pages or apps. File requests go straight to the CDN, never through this API.

Every response has a kind field, "image" or "document". Check it before you read variants.

API base URLhttps://files.example.com/v1
CDN base URLhttps://your-cdn.example.com
Your folderyour-app

2. Authentication ​

Every request needs your API key in the Authorization header:

Authorization: Bearer f360_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

X-API-Key: <key> is accepted as well.

  • Keys are created in the Files360 dashboard and shown only once. Store the key in an environment variable or secret manager.
  • Use keys only from your server. Never put a key in browser JavaScript, a mobile app bundle or a public repository. Browsers and apps upload to your backend, and your backend calls this API.
  • To rotate a key: create a new key in the dashboard, deploy it, then revoke the old one.

Suggested environment variables:

FILES360_API_URL=https://files.example.com
FILES360_API_KEY=f360_...

Test the connection:

bash
curl -s "$FILES360_API_URL/v1/me" -H "Authorization: Bearer $FILES360_API_KEY"

3. Your configuration ​

Every upload produces these sizes:

SizeBox (px)FitResult
thumb320 x 320covercropped to fill the box exactly
medium800 x autoinsidefits inside the box, keeps aspect ratio
large1600 x autoinsidefits inside the box, keeps aspect ratio

Images are never upscaled, so a size can come out smaller than its box. A 600 px wide photo stays 600 px wide in the larger sizes.

SettingValue
Output formatWebP
Quality80
Max file size15 MB
Max resolution60 megapixels
Accepted imagesJPEG, PNG, WebP, GIF, AVIF, TIFF, SVG
Accepted documentsPDF, DOCX, DOC, XLSX, XLS, PPTX, PPT, CSV, TXT
Accepted archivesNone. ZIP uploads answer 415 archives_disabled.
Original keptNo. Only the optimized sizes are stored.
Rate limit120 requests per minute

HEIC files from iPhones are not accepted. Web browsers on iOS usually convert photos to JPEG when uploading through a file input. Native apps should export JPEG before sending.

Settings can change over time. Call GET /v1/me to read the current configuration from code.

4. Endpoints ​

MethodPathWhat it does
GET/v1/meReturns your app configuration. Use it to test a key.
POST/v1/filesUploads a file (multipart): an image is optimized, a document is stored as is.
POST/v1/files/from-urlDownloads a file from a public URL and handles it the same way.
GET/v1/filesLists your files, newest first.
GET/v1/files/:idReturns one file.
DELETE/v1/files/:idDeletes a file and everything stored for it.

Upload a file ​

POST /v1/files with multipart/form-data:

FieldRequiredDescription
fileyesThe image or document file.
foldernoSub folder inside your app folder, e.g. products or users/avatars. Lowercase a-z, 0-9, -, _, up to 5 levels.
refnoYour own reference, e.g. a product or user id (max 120 characters). Lets you find files later with GET /v1/files?ref=.
bash
curl -X POST "$FILES360_API_URL/v1/files" \
  -H "Authorization: Bearer $FILES360_API_KEY" \
  -F "file=@./IMG_2231.jpg" \
  -F "folder=products" \
  -F "ref=product-1042"

Response 201 Created:

json
{
  "id": "66f1c2a9e4b0a1d2c3f4e5a6",
  "kind": "image",
  "folder": "products",
  "ref": "product-1042",
  "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "input": {
    "name": "IMG_2231.jpg",
    "format": "jpeg",
    "width": 4032,
    "height": 3024,
    "bytes": 3482112
  },
  "variants": {
    "thumb": {
      "url": "https://your-cdn.example.com/your-app/products/3f9a1c7e5b2d8a4f6e0c9b1d/thumb.webp",
      "width": 320,
      "height": 320,
      "bytes": 7168,
      "format": "webp"
    },
    "medium": {
      "url": "https://your-cdn.example.com/your-app/products/3f9a1c7e5b2d8a4f6e0c9b1d/medium.webp",
      "width": 800,
      "height": 600,
      "bytes": 33600,
      "format": "webp"
    },
    "large": {
      "url": "https://your-cdn.example.com/your-app/products/3f9a1c7e5b2d8a4f6e0c9b1d/large.webp",
      "width": 1600,
      "height": 1200,
      "bytes": 134400,
      "format": "webp"
    }
  },
  "srcset": "https://your-cdn.example.com/your-app/products/3f9a1c7e5b2d8a4f6e0c9b1d/medium.webp 800w, https://your-cdn.example.com/your-app/products/3f9a1c7e5b2d8a4f6e0c9b1d/large.webp 1600w",
  "bytesOut": 175168,
  "createdAt": "2026-01-15T10:24:00.000Z",
  "duplicate": false
}
  • variants.<size>.url is the public CDN URL of each size.
  • srcset is ready to use in an <img> tag. Cropped sizes are left out because their aspect ratio differs.
  • Uploading the same file into the same folder again returns the existing record with status 200 and "duplicate": true. Retrying an upload is always safe.

Upload a document ​

Same endpoint, same fields. A document is stored as it arrives instead of being resized, and comes back with one URL and a summary of what is inside it.

bash
curl -X POST "$FILES360_API_URL/v1/files" \
  -H "Authorization: Bearer $FILES360_API_KEY" \
  -F "file=@./rental-terms.pdf" \
  -F "folder=contracts" \
  -F "ref=booking-5521"

Response 201 Created:

json
{
  "id": "66f1c2a9e4b0a1d2c3f4e5a7",
  "kind": "document",
  "folder": "contracts",
  "ref": "booking-5521",
  "hash": "9f2b4c8d1e0a7f6b3c5d8e2a4b6c9d1e3f5a7b9c1d3e5f7a9b1c3d5e7f9a1b3c",
  "input": {
    "name": "rental-terms.pdf",
    "format": "pdf",
    "bytes": 184320
  },
  "file": {
    "url": "https://your-cdn.example.com/your-app/contracts/8b1f0c2ad4e69137aa02c5be/rental-terms.pdf",
    "name": "rental-terms.pdf",
    "mime": "application/pdf",
    "bytes": 184320
  },
  "preview": {
    "kind": "pdf",
    "pages": 12
  },
  "bytesOut": 184320,
  "createdAt": "2026-01-15T10:26:00.000Z",
  "duplicate": false
}
  • file.url is the public CDN URL. It is the only URL a document has: there are no sizes and no srcset.
  • The file is byte for byte the one you sent. PDFs open in the browser, the other types download.
  • preview is a summary read once at upload time. Its fields depend on the type:
TypeWhat preview holds
PDFpages
XLSX, CSVrows, columns, and sheets for a workbook
DOCXwords, paragraph count
PPTXslides
TXTlines
ZIPentries, unpackedBytes
DOC, XLS, PPTpages, words, slides when the file carries document properties, plus macros: true when it holds VBA

The 97-2003 formats are stored and served like any other file; only their properties are read, so the library shows a summary card rather than the contents.

  • The last part of the URL is your file name, cleaned up to lowercase letters, digits, ., - and _.

Import from a URL ​

POST /v1/files/from-url with a JSON body. Useful for migrating files you already host somewhere.

bash
curl -X POST "$FILES360_API_URL/v1/files/from-url" \
  -H "Authorization: Bearer $FILES360_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/photo.jpg", "folder": "products", "ref": "product-1042"}'

The URL must be public (http or https). Images and documents both work: what comes back is decided by the bytes that arrive, not by the extension in the URL. The response is the same as for a file upload.

List files ​

GET /v1/files?page=1&limit=50&folder=products&ref=product-1042

All parameters are optional. limit goes up to 200. Images and documents come back in the same list, told apart by kind.

json
{ "page": 1, "limit": 50, "total": 132, "items": [ { "id": "...", "variants": { } } ] }

Get one file ​

GET /v1/files/:id returns the same object as the upload, image or document.

Delete a file ​

bash
curl -X DELETE "$FILES360_API_URL/v1/files/66f1c2a9e4b0a1d2c3f4e5a6" \
  -H "Authorization: Bearer $FILES360_API_KEY"

Response 204 No Content. Every size, or the document itself, is removed from storage. Copies already cached by the CDN edge can stay reachable for a short time.

5. Code examples ​

Node.js 20+: upload a file from disk ​

js
const fs = require('node:fs/promises');
const path = require('node:path');

async function uploadImage(filePath, { folder, ref } = {}) {
  const form = new FormData();
  form.append('file', new Blob([await fs.readFile(filePath)]), path.basename(filePath));
  if (folder) form.append('folder', folder);
  if (ref) form.append('ref', ref);

  const res = await fetch(process.env.FILES360_API_URL + '/v1/files', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.FILES360_API_KEY },
    body: form,
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data.error.code + ': ' + data.error.message);
  return data;
}

uploadImage('./IMG_2231.jpg', { folder: 'products', ref: 'product-1042' })
  .then((image) => console.log(image.variants.large.url));

Node.js with Express: forward a user upload ​

The browser uploads to your app as usual, your route forwards the file.

js
const express = require('express');
const multer = require('multer');

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 15 * 1024 * 1024 },
});

const router = express.Router();

router.post('/products/:id/photos', upload.single('photo'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No photo received' });

  const form = new FormData();
  form.append('file', new Blob([req.file.buffer], { type: req.file.mimetype }), req.file.originalname);
  form.append('folder', 'products');
  form.append('ref', req.params.id);

  const apiRes = await fetch(process.env.FILES360_API_URL + '/v1/files', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.FILES360_API_KEY },
    body: form,
  });
  const image = await apiRes.json();
  if (!apiRes.ok) return res.status(apiRes.status).json(image);

  // Save what you need on your own record, for example:
  // await Product.updateOne(
  //   { _id: req.params.id },
  //   { $push: { photos: { imageId: image.id, thumb: image.variants.thumb.url, large: image.variants.large.url, srcset: image.srcset } } }
  // );

  res.status(201).json(image);
});

module.exports = router;

Node.js: delete when your record is deleted ​

js
async function deleteImage(imageId) {
  const res = await fetch(process.env.FILES360_API_URL + '/v1/files/' + imageId, {
    method: 'DELETE',
    headers: { Authorization: 'Bearer ' + process.env.FILES360_API_KEY },
  });
  if (res.status !== 204 && res.status !== 404) {
    const data = await res.json();
    throw new Error(data.error.message);
  }
}

PHP (WordPress, Laravel or plain PHP) with cURL ​

php
<?php
function upload_image(string $path, string $folder = '', string $ref = ''): array
{
    $fields = ['file' => new CURLFile($path, mime_content_type($path), basename($path))];
    if ($folder !== '') $fields['folder'] = $folder;
    if ($ref !== '') $fields['ref'] = $ref;

    $ch = curl_init(getenv('FILES360_API_URL') . '/v1/files');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 60,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FILES360_API_KEY')],
        CURLOPT_POSTFIELDS => $fields,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    if ($body === false) {
        throw new RuntimeException('Image API unreachable: ' . curl_error($ch));
    }
    $data = json_decode($body, true);
    if ($status >= 400) {
        throw new RuntimeException($data['error']['code'] . ': ' . $data['error']['message']);
    }
    return $data;
}

$image = upload_image('/tmp/IMG_2231.jpg', 'products', 'product-1042');
echo $image['variants']['large']['url'];

HTML: responsive image ​

html
<img
  src="https://.../large.webp"
  srcset="(value of image.srcset)"
  sizes="(max-width: 768px) 100vw, 800px"
  width="1600" height="1200"
  loading="lazy"
  alt="Describe the image">

Set width and height from the returned size to avoid layout shift.

6. Errors ​

Errors always return JSON in this shape:

json
{ "error": { "code": "file_too_large", "message": "File exceeds the 15 MB limit" } }

Use code in your logic, show or log message.

StatusCodeMeaningRetry?
400missing_fileNo file field in the multipart bodyNo
400invalid_uploadMore than one file or a wrong field nameNo
400invalid_folder / invalid_refFolder or ref has invalid characters or lengthNo
400invalid_url / blocked_url / fetch_failedURL import could not download the fileNo
400invalid_jsonJSON body could not be parsedNo
401missing_api_key / invalid_api_keyKey missing, wrong or revokedNo
403app_disabledThe app is disabled in the dashboardNo
404not_foundImage does not exist or belongs to another appNo
413file_too_large / image_too_largeAbove 15 MB or 60 megapixelsNo
415unsupported_image / unsupported_format / heic_not_supportedNot an accepted imageNo
415unsupported_documentA recognised but unsupported document, e.g. .pptx, .doc or a macro-enabled workbookNo
415documents_disabledThis app takes images onlyNo
415archives_disabledThis app does not accept ZIP archivesNo
422processing_failedThe image is corruptNo
429rate_limitedMore than 120 requests in a minuteYes, after 60 s
502storage_errorStorage did not accept the filesYes, with backoff
503busyToo many images in the processing queue. Documents never queueYes, with backoff
504fetch_timeoutURL import timed outYes

For retries, wait 1 s, then 2 s, then 4 s, and stop after 4 attempts. Uploads are deduplicated, so a retry never creates a second copy.

7. Good practices ​

  • Store the id. Save id together with the URLs you use, so you can delete the file when your record is deleted.
  • Branch on kind. image gives you variants and srcset, document gives you file.url.
  • URLs never change. They are cached for a year. To replace a file, upload the new one, save its URLs, then delete the old record.
  • Use folders for structure, e.g. products, users/avatars, events/2026. Folders only group files, they have no permissions.
  • Use ref to link files to your own records and find them with GET /v1/files?ref=....
  • Keep uploads server side and protect your own upload route (auth, size limit, rate limit).
  • Set a timeout of about 60 seconds on upload requests. Large photos take a few seconds to process.

Released under the AGPL-3.0 licence.