Files

The files API covers the full lifecycle: upload, retrieve, list, update, copy, replace, download, restore and delete.

The file object

json
{
  "id": "file_8f2b…",
  "filename": "avatar.jpg",
  "path": "users/avatars",
  "contentType": "image/jpeg",
  "size": 245123,
  "status": "uploaded",
  "version": 1,
  "metadata": { "userId": "u_123" },
  "createdAt": "2026-01-02T10:00:00.000Z",
  "updatedAt": "2026-01-02T10:00:00.000Z"
}

path is the logical folder path (null for root-level files). metadata is custom key/value data, or null. version increments each time content is replaced. uploadedAt and checksum appear once the file is uploaded. deletedAt appears on deleted files.

Upload a file

Uploads are a three-step handshake so file bytes never flow through APULODI's application servers.

1 — Request an upload URL

http
POST /v1/files/upload
json
{
  "filename": "avatar.jpg",
  "contentType": "image/jpeg",
  "size": 245123,
  "path": "users/avatars",
  "metadata": { "userId": "u_123" }
}
bash
curl -X POST https://app.apulodi.dev/v1/files/upload \
  -H "Authorization: Bearer $APULODI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "avatar.jpg",
    "contentType": "image/jpeg",
    "size": 245123,
    "path": "users/avatars",
    "metadata": { "userId": "u_123" }
  }'

Returns 201 Created with the PENDING file and a short-lived presigned URL:

json
{
  "file": {
    "id": "file_8f2b…",
    "status": "pending",
    "filename": "avatar.jpg",
    "contentType": "image/jpeg",
    "size": 245123,
    "path": "users/avatars",
    "version": 1,
    "metadata": { "userId": "u_123" },
    "createdAt": "2026-01-02T10:00:00.000Z",
    "updatedAt": "2026-01-02T10:00:00.000Z"
  },
  "upload": {
    "url": "https://…",
    "method": "PUT",
    "headers": { "content-type": "image/jpeg" },
    "expiresAt": "2026-01-02T10:15:00.000Z"
  }
}

2 — PUT the bytes to storage, directly

bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @avatar.jpg

The presigned URL expires quickly — upload soon, or request a fresh one from a new upload call. Reuse filename + size with the same Idempotency-Key to retry safely.

3 — Complete the upload

http
POST /v1/files/:id/complete
bash
curl -X POST https://app.apulodi.dev/v1/files/file_8f2b…/complete \
  -H "Authorization: Bearer $APULODI_API_KEY"

APULODI HEADs the object, verifies its real size and content type, then transitions the file to uploaded:

json
{
  "data": {
    "id": "file_8f2b…",
    "status": "uploaded",
    "uploadedAt": "2026-01-02T10:00:02.000Z",
    "checksum": "8a3f…",
    "version": 1
  }
}

Retrieve a file

http
GET /v1/files/:id
bash
curl https://app.apulodi.dev/v1/files/file_8f2b… \
  -H "Authorization: Bearer $APULODI_API_KEY"

Returns the file object under data.

List files

http
GET /v1/files?limit=20&cursor=…&search=…&path=…&status=…&contentType=…&sortBy=…&order=…

All parameters are optional:

ParamTypeDefaultNotes
limitint201–100
cursorstring—opaque pagination cursor
searchstring—substring match on filename
pathstring—exact logical folder path
statusstring—pending / uploaded / failed / deleted
contentTypestring—exact MIME type
sortBystringcreatedAtcreatedAt / updatedAt / filename / size
orderstringdescasc / desc
bash
curl "https://app.apulodi.dev/v1/files?path=users/avatars&sortBy=size&order=desc&limit=5" \
  -H "Authorization: Bearer $APULODI_API_KEY"
json
{
  "data": [ { "id": "file_8f2b…", "filename": "avatar.jpg", "status": "uploaded" } ],
  "pagination": { "hasMore": true, "nextCursor": "…" }
}

See Pagination for the full walkthrough.

Update a file

Rename, move to another logical path, and/or replace custom metadata:

http
PATCH /v1/files/:id
json
{
  "filename": "new-name.jpg",
  "path": "products/images",
  "metadata": { "sku": "ABC-123" }
}
bash
curl -X PATCH https://app.apulodi.dev/v1/files/file_8f2b… \
  -H "Authorization: Bearer $APULODI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"new-name.jpg","path":"products/images","metadata":{"sku":"ABC-123"}}'

Renames and moves are logical only — the storage key generated by APULODI never changes, so no bytes are moved. Any field is optional, but at least one must be present. Returns the updated file object.

CDN delivery (visibility)

Every file is private by default: downloads use short-lived, authenticated presigned URLs. To serve a file publicly — embedded in apps, <img> tags, CSS, or shared links — flip its visibility:

http
PATCH /v1/files/:id
json
{ "visibility": "PUBLIC" }
bash
curl -X PATCH https://app.apulodi.dev/v1/files/file_8f2b… \
  -H "Authorization: Bearer $APULODI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"visibility":"PUBLIC"}'

A public file (once uploaded) gains a publicUrl:

json
{
  "visibility": "public",
  "publicUrl": "https://cdn.apulodi.dev/organizations/…/files/file_8f2b…/v1"
}

The URL is:

  • Unguessable — the key is generated server-side from identities; it never contains user-controlled segments
  • Immutable per version — replacing content writes a new versioned key and therefore a new URL, so old URLs keep serving the old version
  • Edge-cached for a year — safe to embed anywhere for that version

Revert with {"visibility": "PRIVATE"}: the public URL stops being served to API consumers, cached copies expire with the TTL, and downloads fall back to authenticated presigned URLs.

Transformed variants inherit their file's visibility: a variant of a public file gets the same treatment on its variant download URL.

Copy a file

Server-side copy to the same or another folder — no bytes flow through the API:

http
POST /v1/files/:id/copy
json
{ "path": "backups", "filename": "avatar-copy.jpg" }
bash
curl -X POST https://app.apulodi.dev/v1/files/file_8f2b…/copy \
  -H "Authorization: Bearer $APULODI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":"backups","filename":"avatar-copy.jpg"}'

Returns 201 Created with a new file object sharing the same bytes.

Download URL

http
POST /v1/files/:id/download-url
json
{ "expiresInSeconds": 300 }

expiresInSeconds is optional (60–86400). Returns a presigned GET URL your users fetch directly from storage:

json
{
  "url": "https://…",
  "expiresAt": "2026-01-02T10:05:00.000Z"
}
bash
curl -X POST https://app.apulodi.dev/v1/files/file_8f2b…/download-url \
  -H "Authorization: Bearer $APULODI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expiresInSeconds":300}'

Replace content (versioning)

Replacing a file's content keeps the same file id, bumps version, and preserves the previous object in storage.

Step 1 — initiate:

http
POST /v1/files/:id/replace
json
{ "contentType": "image/jpeg", "size": 212000 }
bash
curl -X POST https://app.apulodi.dev/v1/files/file_8f2b…/replace \
  -H "Authorization: Bearer $APULODI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contentType":"image/jpeg","size":212000}'

Returns { "file": …, "upload": { "url": …, "method": "PUT", "headers": … } }.

Step 2 — PUT the new bytes to upload.url.

Step 3 — finalize:

http
POST /v1/files/:id/replace/complete
bash
curl -X POST https://app.apulodi.dev/v1/files/file_8f2b…/replace/complete \
  -H "Authorization: Bearer $APULODI_API_KEY"

Returns the file with version: 2.

Delete a file

Deleting soft-deletes the file: it disappears from listings immediately and all reads return 404, but the underlying storage object is kept for a grace window (APULODI_DELETE_GRACE_DAYS, default 7 days) so the file can be restored. The object is purged by a periodic sweep after the window elapses (a file.purged event is emitted).

http
DELETE /v1/files/:id
bash
curl -X DELETE https://app.apulodi.dev/v1/files/file_8f2b… \
  -H "Authorization: Bearer $APULODI_API_KEY"
json
{ "data": { "id": "file_8f2b…", "deleted": true } }

Restore a file

Restores a soft-deleted file while it is still within the delete grace window (its object still exists). Once the purge sweep has removed the object, restore fails with 409 OBJECT_NOT_FOUND.

http
POST /v1/files/:id/restore
bash
curl -X POST https://app.apulodi.dev/v1/files/file_8f2b…/restore \
  -H "Authorization: Bearer $APULODI_API_KEY"

Returns the file with status: "uploaded".

Errors

StatusCodeWhy
400VALIDATION_ERRORInvalid body or query parameters
404FILE_NOT_FOUNDNo such file in your project (or it was deleted)
409FILE_NOT_PENDINGcomplete on a file that isn't pending
409FILE_NOT_UPLOADEDAction requires an uploaded file
409REPLACE_IN_PROGRESSA replacement is already pending for this file
409NO_REPLACE_IN_PROGRESSreplace/complete without an initiated replacement
413FILE_TOO_LARGEExceeds the multipart size limit
422NOT_PROCESSABLETransform on a non-image (or SVG) file

Image processing

Image files (jpeg, png, webp, avif, gif, tiff — not SVG) support variants: derived copies produced asynchronously from the original, which is never modified. Identical transformation requests are idempotent — the same params map to the same variant, so retries never duplicate work. Every eligible upload automatically gets a 256px webp thumbnail.

Request a transform

bash
curl -X POST https://app.apulodi.dev/v1/files/file_8f2b…/transform \
  -H "Authorization: Bearer $APULODI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"width": 256, "height": 256, "fit": "cover", "format": "webp", "quality": 80}'

Only width or height is required; defaults are fit: "cover", format: "webp", quality: 80. Dimensions are capped at 4096px. Processing is asynchronous: the call returns 202 with the variant in pending, and the file.processed (or file.processing_failed) event fires when it finishes.

List variants

bash
curl https://app.apulodi.dev/v1/files/file_8f2b…/variants \
  -H "Authorization: Bearer $APULODI_API_KEY"

Returns each variant with its label (e.g. w256_cover_webp_q80), status (pending → processing → ready/failed), output dimensions, and size.

Download a variant

bash
curl -X POST https://app.apulodi.dev/v1/files/file_8f2b…/variants/var_c1d9…/download-url \
  -H "Authorization: Bearer $APULODI_API_KEY"

Returns a presigned GET URL — only ready variants qualify (otherwise 409 VARIANT_NOT_READY). Variant downloads are metered as bandwidth.

Next: Folders — the logical folder tree.