Files
The files API covers the full lifecycle: upload, retrieve, list, update, copy, replace, download, restore and delete.
The file object
{
"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
POST /v1/files/upload
{
"filename": "avatar.jpg",
"contentType": "image/jpeg",
"size": 245123,
"path": "users/avatars",
"metadata": { "userId": "u_123" }
}
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:
{
"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
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
POST /v1/files/:id/complete
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:
{
"data": {
"id": "file_8f2b…",
"status": "uploaded",
"uploadedAt": "2026-01-02T10:00:02.000Z",
"checksum": "8a3f…",
"version": 1
}
}
Retrieve a file
GET /v1/files/:id
curl https://api.apulodi.com/v1/files/file_8f2b… \
-H "Authorization: Bearer $APULODI_API_KEY"
Returns the file object under data.
List files
GET /v1/files?limit=20&cursor=…&search=…&path=…&status=…&contentType=…&sortBy=…&order=…
All parameters are optional:
| Param | Type | Default | Notes |
|---|---|---|---|
limit | int | 20 | 1–100 |
cursor | string | — | opaque pagination cursor |
search | string | — | substring match on filename |
path | string | — | exact logical folder path |
status | string | — | pending / uploaded / failed / deleted |
contentType | string | — | exact MIME type |
sortBy | string | createdAt | createdAt / updatedAt / filename / size |
order | string | desc | asc / desc |
curl "https://api.apulodi.com/v1/files?path=users/avatars&sortBy=size&order=desc&limit=5" \
-H "Authorization: Bearer $APULODI_API_KEY"
{
"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:
PATCH /v1/files/:id
{
"filename": "new-name.jpg",
"path": "products/images",
"metadata": { "sku": "ABC-123" }
}
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:
POST /v1/files/:id/copy
{ "path": "backups", "filename": "avatar-copy.jpg" }
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
POST /v1/files/:id/download-url
{ "expiresInSeconds": 300 }
expiresInSeconds is optional (60–86400). Returns a presigned GET URL your
users fetch directly from storage:
{
"url": "https://…",
"expiresAt": "2026-01-02T10:05:00.000Z"
}
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:
POST /v1/files/:id/replace
{ "contentType": "image/jpeg", "size": 212000 }
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:
POST /v1/files/:id/replace/complete
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).
DELETE /v1/files/:id
curl -X DELETE https://api.apulodi.com/v1/files/file_8f2b… \
-H "Authorization: Bearer $APULODI_API_KEY"
{ "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.
POST /v1/files/:id/restore
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
| Status | Code | Why |
|---|---|---|
400 | VALIDATION_ERROR | Invalid body or query parameters |
404 | FILE_NOT_FOUND | No such file in your project (or it was deleted) |
409 | FILE_NOT_PENDING | complete on a file that isn't pending |
409 | FILE_NOT_UPLOADED | Action requires an uploaded file |
409 | REPLACE_IN_PROGRESS | A replacement is already pending for this file |
409 | NO_REPLACE_IN_PROGRESS | replace/complete without an initiated replacement |
413 | FILE_TOO_LARGE | Exceeds the multipart size limit |
422 | NOT_PROCESSABLE | Transform 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
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
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
(pending → processing → ready/failed), output dimensions, and size.
Download a variant
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.