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://api.apulodi.com/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://api.apulodi.com/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://api.apulodi.com/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
cursorstringopaque pagination cursor
searchstringsubstring match on filename
pathstringexact logical folder path
statusstringpending / uploaded / failed / deleted
contentTypestringexact MIME type
sortBystringcreatedAtcreatedAt / updatedAt / filename / size
orderstringdescasc / desc
bash
curl "https://api.apulodi.com/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://api.apulodi.com/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.

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://api.apulodi.com/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://api.apulodi.com/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://api.apulodi.com/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://api.apulodi.com/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://api.apulodi.com/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://api.apulodi.com/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://api.apulodi.com/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://api.apulodi.com/v1/files/file_8f2b…/variants \
  -H "Authorization: Bearer $APULODI_API_KEY"

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

Download a variant

bash
curl -X POST https://api.apulodi.com/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.