Skip to main content

File Management

Looking for the product view?
The Help Center explains this surface as users see it: Logs, Snapshots & Files.

Apps store their files (HTML, CSS, JavaScript, images, etc.) in an associated Library. The file management endpoints provide CRUD operations for app content.

Important Concepts​

Library Association: Each app has a libraryId field that references its file storage. Apps without a library cannot use file management endpoints.

Path Structure: Files are organized hierarchically using forward-slash paths (e.g., index.html, css/styles.css, images/logo.png).

Content Types: Files have MIME types that determine how they're served. The system auto-detects types from file extensions.

Content Recency: File operations do NOT modify the app row. The app's content-modified time is exposed as the derived contentModifiedAt field — the latest modifiedAt across the app's library files (null when the library has no files). The app's own updatedAt/defnUpdatedAt reflect only changes to the app row itself (e.g. draft commits), not file edits.


GET /api/apps/{id}/contents/{path*}​

Read a file from the app's library.

Authentication: Required (session or token)

Permissions Required: None for asset paths (read access to the app is enough). Server-side paths additionally require app:write: the manifest (informer.yaml or informer.yml), the handler folders (server/, webhooks/, tools/, mcp/, migrations/, channels/, embeddings/), and _bundles/, where deploy writes the bundled handlers. Run-level shared users can execute the app but must not read its server code.

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
pathstringFile path within the library (e.g., index.html, css/styles.css)

Query Parameters:

ParameterTypeDefaultDescription
downloadbooleanfalseForce download instead of inline display

Response:

Returns the file content as a binary stream with appropriate Content-Type header.

Example Request:

GET /api/apps/analytics:sales-dashboard/contents/index.html

Example Response:

HTTP/1.1 200 OK
Content-Type: text/html

<!DOCTYPE html>
<html>
<head><title>Sales Dashboard</title></head>
<body>...</body>
</html>

Download Example:

GET /api/apps/analytics:sales-dashboard/contents/logo.png?download=true
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: attachment; filename="logo.png"

[binary data]

Error Responses:

  • 400 Bad Request - App has no associated library
  • 404 Not Found - App doesn't exist, user lacks access, or file not found
  • 403 Forbidden - User lacks run permission

PUT /api/apps/{id}/contents/{path*}​

Write or create a file in the app's library.

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
pathstringFile path within the library

Payload:

{
"content": "<!DOCTYPE html>\n<html>...",
"contentType": "text/html",
"encoding": "utf8"
}

Payload Fields:

FieldTypeRequiredDefaultDescription
contentstringYes-File content (can be empty string)
contentTypestringNoAuto-detectMIME type
encodingstringNoutf8utf8 or base64
stagingLibraryIdstringNo-Id returned by POST /files/_stage. Claims the staged deploy, which sends this write into the staging library instead of the live one

Returns the created/updated file metadata with status 201 Created (new file) or 200 OK (updated file).

This route is one of only two write paths that can be redirected into a staging library (the other is POST /_upload), and the redirect is claimed rather than automatic. While an app has a staged deploy, a write that presents the matching stagingLibraryId lands in the staging library; a write that presents none, or the wrong one, is refused with 409 Conflict rather than accepted into a live library the promotion is about to replace. See POST /files/_stage.

Write a filePUT /api/apps/admin%3Aorder-desk/contents/orders.html
Upsert: 201 when the file is created, 200 when overwritten. Writing informer.yaml additionally runs the manifest license gate and eagerly re-binds dependencies.
Request body
{
"content": "<!DOCTYPE html>\n<html>\n <head><title>Order Desk</title></head>\n <body><h1>Open Orders</h1></body>\n</html>"
}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/contents/orders.html"
}
},
"extension": "html",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"embedded": false,
"directory": false,
"libraryId": "00000000-0000-4000-8000-000000000002",
"parentId": null,
"filename": "orders.html",
"contentType": "text/html",
"modifiedAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"lobId": "16400",
"ownerId": null,
"data": {},
"templateId": null,
"role": null,
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"description": null,
"assistantId": null,
"integrationId": null,
"remoteId": null,
"remoteUrl": null,
"size": 524288,
"indexed": false,
"indexingStartedAt": null,
"indexedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"messageId": null,
"chatId": null,
"expiresAt": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

The modifiedAt field is the per-file content timestamp that backs the app's derived contentModifiedAt (MAX(file.modifiedAt) across the library — see Content Recency above).

Example - Create HTML File:

PUT /api/apps/analytics:sales-dashboard/contents/index.html
Content-Type: application/json

{
"content": "<!DOCTYPE html>\n<html>\n <head><title>Dashboard</title></head>\n <body><h1>Sales Dashboard</h1></body>\n</html>",
"contentType": "text/html"
}

Example - Upload Binary File (Base64):

PUT /api/apps/analytics:sales-dashboard/contents/logo.png
Content-Type: application/json

{
"content": "iVBORw0KGgoAAAANSUhEUgAAAAUA...",
"contentType": "image/png",
"encoding": "base64"
}

Side Effects:

  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt
  • Creates parent directories as needed
  • Overwrites existing file if present
  • Writing informer.yaml runs the manifest license gate first (Apps-tier content is rejected with 403 + apps_license_required on a Magic Report or unlicensed tenant) and, on success, eagerly re-binds the app's dependency rows so manifest edits take effect in preview without a deploy

Error Responses:

  • 400 Bad Request - App has no library, or invalid payload
  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks write permission
  • 409 Conflict - A deploy is staged for this app and the request presented no stagingLibraryId, or one that is not the app's current staged upload

PATCH /api/apps/{id}/contents/{path*}​

Perform a patch operation on an existing file (replace, append, prepend, or insert text).

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
pathstringFile path within the library

Payload:

The payload structure depends on the operation:

Replace Operation​

{
"operation": "replace",
"search": "oldText",
"replacement": "newText",
"replaceAll": false
}
FieldTypeRequiredDefaultDescription
operationstringYes-Must be "replace"
searchstringYes-Text to search for
replacementstringYes-Replacement text (can be empty)
replaceAllbooleanNofalseReplace all occurrences (default: first only)

Append Operation​

{
"operation": "append",
"text": "Text to add at end"
}

Prepend Operation​

{
"operation": "prepend",
"text": "Text to add at beginning"
}

Insert Operation​

{
"operation": "insert",
"text": "Text to insert",
"insertAt": 100
}
FieldTypeRequiredDescription
operationstringYesMust be "insert"
textstringYesText to insert
insertAtintegerYesCharacter position (0-based)
Patch a filePATCH /api/apps/00000000-0000-4000-8000-000000000001/contents/orders.html
Operations: replace (search/replacement/replaceAll), append (text), prepend (text), insert (text/insertAt). The file must already exist.
Request body
{
"operation": "replace",
"search": "Open Orders",
"replacement": "Open Orders Today"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/contents/orders.html"
}
},
"extension": "html",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"filename": "orders.html",
"embedded": false,
"contentType": "text/html",
"lobId": "16400",
"role": null,
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"integrationId": null,
"directory": false,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": null,
"libraryId": "00000000-0000-4000-8000-000000000003",
"remoteId": null,
"remoteUrl": null,
"size": 524288,
"modifiedAt": "2026-01-15T16:20:00.000Z",
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

Side Effects:

  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt
  • File must exist (PATCH doesn't create new files)
  • Cannot be redirected into a staging library, because a patch is a read-modify-write and a fresh staging library is empty. While a deploy is staged this route is refused outright with 409 Conflict; see POST /files/_stage

Error Responses:

  • 400 Bad Request - Invalid operation or missing required fields
  • 404 Not Found - App or file doesn't exist
  • 403 Forbidden - User lacks write permission
  • 409 Conflict - A deploy is staged for this app, so the patch would be discarded when it completes

GET /api/apps/{id}/files​

List files in the app's library. Lists the library root by default; descend with parentId/dir, or search the whole tree with q.

Authentication: Required

Permissions Required: app:run (any user who can run the app)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Query Parameters:

ParameterTypeDefaultDescription
parentIdstringnullList children of this directory (null = root)
dirstringnullList children of this directory path
qstring—Search the whole tree by filename
start / endnumber0 / 100Paging window
dirOnlybooleanfalseOnly return directories
List an App's filesGET /api/apps/00000000-0000-4000-8000-000000000001/files
Lists the library ROOT by default; pass parentId (or dir) to descend into a directory, q to search the whole tree, dirOnly to filter to directories.
Response · 200
"[{\"id\":\"00000000-0000-4000-8000-000000000002\",\"filename\":\"pages\",\"canIndex\":true,\"remoteUrl\":null,\"directory\":true,\"contentType\":\"application/vnd.apple.pages\",\"indexed\":false,\"modifiedAt\":\"2026-01-15T16:20:00.000Z\",\"indexingErroredAt\":null,\"indexingErrorMessage\":null,\"indexingStartedAt\":null,\"indexedAt\":null,\"path\":\"/pages\",\"size\":null,\"runningDescendantsCount\":0},{\"id\":\"00000000-0000-4000-8000-000000000003\",\"filename\":\"index.html\",\"canIndex\":true,\"remoteUrl\":null,\"directory\":false,\"contentType\":\"text/html\",\"indexed\":false,\"modifiedAt\":\"2026-01-15T16:20:00.000Z\",\"indexingErroredAt\":null,\"indexingErrorMessage\":null,\"indexingStartedAt\":null,\"indexedAt\":null,\"path\":\"/index.html\",\"size\":737,\"runningDescendantsCount\":0},{\"id\":\"00000000-0000-4000-8000-000000000004\",\"filename\":\"informer.yaml\",\"canIndex\":true,\"remoteUrl\":null,\"directory\":false,\"contentType\":\"text/yaml\",\"indexed\":false,\"modifiedAt\":\"2026-01-15T16:20:00.000Z\",\"indexingErroredAt\":null,\"indexingErrorMessage\":null,\"indexingStartedAt\":null,\"indexedAt\":null,\"path\":\"/informer.yaml\",\"size\":283,\"runningDescendantsCount\":0}]"
Captured from the API examples test suite; ids and timestamps are normalized.

Key Fields:

FieldDescription
directorytrue if this is a directory, false for files
parentIdUUID of parent directory (null for root level)
contentTypeMIME type (only present for files)
sizeFile size in bytes (only present for files)

Behavior:

  • Always lists the app's live library. A staged deploy never affects this route: it keeps returning the build that is currently serving, and it never answers 409. The staging library's files are not visible here until the deploy promotes them.

Error Responses:

  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks write permission

DELETE /api/apps/{id}/files/{fileId}​

Delete a file or directory from the app's library.

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
fileIdstringFile UUID (from the files list)
Delete a fileDELETE /api/apps/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002
Deleting a directory removes its whole subtree.
Response · 204 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

Behavior:

  • Deleting a directory also deletes all contained files and subdirectories
  • Does NOT modify the app row
  • Refused while a deploy is staged; see POST /files/_stage

Error Responses:

  • 404 Not Found - App or file doesn't exist
  • 403 Forbidden - User lacks write permission
  • 409 Conflict - A deploy is staged for this app, so the delete would be undone when the staged build is promoted

POST /api/apps/{id}/files/_stage​

Open a staging library for a deploy and point the app at it.

The two write paths a deploy uses, PUT /contents/{path*} and POST /_upload, can be redirected into the staging library while it exists, so the app goes on serving its current library, complete and untouched, for the whole length of a deploy. POST /api/apps/\{id\}/_deploy then consumes it, provided it presents this library's id as stagingLibraryId: the live library's files are dropped, the staged files are re-parented onto it and the route table is rebuilt, all inside one transaction. A reader sees the previous build right up to the commit, then the new one.

The files move; the library itself does not. The app's libraryId never changes, so everything attached to that library - shares, favorites, tags, comments, assistants and snapshots - is unaffected by a deploy.

This replaces the older wipe-then-upload sequence, where /files/_clear emptied the live library in its own committed transaction and every request between that commit and the end of the deploy was served from an empty or partly filled tree.

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps). Same level as the sibling upload routes. Staging writes no app content, it only allocates the target the upload pass writes into.

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Behavior:

  • Responds 201 with the newly created staging library. Keep its id: a deploy has to present it as stagingLibraryId to promote the upload.
  • The app's libraryId is left alone. Only stagingLibraryId is set, and it is NULL again once a deploy consumes it.
  • Reads are never redirected. GET /contents, GET /files, the app's assets and its deployed route handlers all continue to resolve the live library, for the whole length of a deploy.
  • An app that already has a staged upload is refused with 409 Conflict. Staging is never taken over: replacing a staged upload silently is how two concurrent deploys interleave, with the loser's files still arriving while the winner publishes a mixture of both builds. Clearing is an explicit act, so a deploy that finds a leftover staged upload calls DELETE /files/_stage first and stages again.
  • Deleting a staging library clears stagingLibraryId through the foreign key's AFTER DELETE trigger, so an abandoned one can be removed safely.

Writes during a staging window​

The redirect is claimed, not automatic. Only the deploy that opened the staging library knows its id, so only that deploy can write into it. Anyone else writing during the window is refused instead of having their work published as part of someone else's build, or destroyed along with a discarded staging library, under a 2xx.

While a deploy is stagedBehavior
PUT /contents/{path*} and POST /_upload with the matching stagingLibraryIdWrite lands in the staging library
The same two routes without it, or with a stale id409 Conflict
Every other app content write409 Conflict
Every read (GET /contents/{path*}, GET /files, GET /icon, the app's assets and deployed routes)Unaffected; serves the live build

The app content writes that answer 409 outright are PATCH /contents/{path*}, POST /files, PUT /files/{fileId}, DELETE /files/{fileId}, POST /files/_clear, POST /snapshots/{snapshotId}/_restore, PUT /icon, DELETE /icon, POST /draft/_commit and POST /_modernize-manifest.

The refusal follows the files, not the route prefix, so the Library API is refused on the same grounds when it reaches an app's live library: see Library File Management. Installing or updating a marketplace pack onto the app is refused too, before the install applies anything.

Error Responses:

  • 400 Bad Request - The app has no associated library
  • 403 Forbidden - User lacks write permission
  • 404 Not Found - App doesn't exist
  • 409 Conflict - The app already has a staged upload. Another deploy may be in progress; discard it with DELETE below before staging again.

Examples:

curl -X POST "${SERVER_URL}/api/apps/myapp/files/_stage" \
-H "Authorization: Basic ${AUTH}"

DELETE /api/apps/{id}/files/_stage​

Discard a staged upload and unlock the app's content routes.

A staging window has no natural end if the deploy that opened it never finishes. Until it is cleared, every content write on the app is refused: the two routes that can redirect only do so for a caller presenting the staging library's id, which a dead deploy is no longer around to do, and the rest answer 409 outright. Reads carry on serving the live build, so the app looks healthy while nobody can change it. The CLI calls this when a deploy fails or is interrupted; an operator can call it to recover an app left in that state.

Authentication: Required

Permissions Required: app:write

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Query Parameters:

ParameterTypeDescription
stagingLibraryIdstringOnly discard if this is still the app's staged upload. Omit to discard whatever is staged.

Behavior:

  • Responds 204. Idempotent: an app with nothing staged is a no-op, so a client can call it unconditionally from a cleanup path.
  • Passing stagingLibraryId is how a deploy avoids discarding a different deploy's in-flight upload; a mismatch responds 409 and discards nothing.
  • Abandoned staged uploads are also swept by the app-sandbox-cleanup system job, which discards any staged upload older than 24 hours. The job runs once a day, at 00:17, so an upload staged just after a sweep waits for the one after next: the worst case before collection is about 48 hours, not 24. Calling this route is how you get the app back immediately.

Error Responses:

  • 403 Forbidden - User lacks write permission
  • 404 Not Found - App doesn't exist
  • 409 Conflict - stagingLibraryId no longer matches the app's staged upload

Examples:

Discard whatever is staged (what an operator recovering a wedged app does):

curl -X DELETE "${SERVER_URL}/api/apps/myapp/files/_stage" \
-H "Authorization: Basic ${AUTH}"

Discard only this deploy's own upload, leaving a newer one alone:

curl -X DELETE "${SERVER_URL}/api/apps/myapp/files/_stage?stagingLibraryId=${STAGING_LIBRARY_ID}" \
-H "Authorization: Basic ${AUTH}"

POST /api/apps/{id}/files/_clear​

Bulk-delete every file in the app's library in a single transaction. Intended for callers that need to wipe the library before re-uploading a fresh tree. Deploy workflows should prefer /files/_stage above, which keeps the app serving while the new build uploads; this route empties the library the app is actively reading from. One round-trip replaces N parallel per-file DELETEs; like the per-file path, the bulk-clear leaves the app row untouched (see Content Recency above).

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps). Same level as the per-file DELETE — bulk-clear is semantically a multi-row variant of it, not an app-lifecycle action.

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
Clear an App's libraryPOST /api/apps/00000000-0000-4000-8000-000000000001/files/_clear
Bulk-deletes every file in one transaction. Idempotent. CLI deploys stage instead (POST /files/_stage), so the app keeps serving its current build; this route empties the live library and is refused while a deploy is staged.
Response · 204 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

Behavior:

  • Files and directories are removed in a single SQL DELETE statement scoped to the app's library; the per-row file.parentId → file.id cascade clears the whole tree without ordering concerns.
  • Large objects (LOBs) for files with stored content are unlinked via the per-row delete_file_lob trigger as part of the cascade.
  • The bulk delete does NOT touch the app row. After it completes the library is empty, so the app's derived contentModifiedAt becomes null.
  • Idempotent: calling on an already-empty library returns 204 with no state change.
  • Refused while a deploy is staged; see POST /files/_stage

Error Responses:

  • 400 Bad Request - The app has no associated library
  • 403 Forbidden - User lacks write permission
  • 404 Not Found - App doesn't exist
  • 409 Conflict - A deploy is staged for this app, so emptying the live library now would leave it serving nothing until the staged build is promoted

Examples:

curl -X POST "${SERVER_URL}/api/apps/myapp/files/_clear" \
-H "Authorization: Basic ${AUTH}"

POST /api/apps/{id}/files​

Create a file or directory under a given parentId. Polymorphic — passing directory: true makes a directory; otherwise an empty file is created. Both share the same endpoint because directories are just File rows with directory: true.

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Request Body:

FieldTypeRequiredDefaultDescription
filenamestringYes-Single-segment basename (no slashes). Max 255 chars.
parentIdstring | nullNonullContaining directory id; null means library root
directorybooleanNofalsetrue to create a directory; false for a file
Create a directoryPOST /api/apps/00000000-0000-4000-8000-000000000001/files
POST /files creates files and directories; sibling-name collisions are 409. PUT /contents is the upsert path for file content.
Request body
{
"filename": "pages",
"directory": true
}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/files"
}
},
"extension": "",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"embedded": false,
"libraryId": "00000000-0000-4000-8000-000000000003",
"parentId": null,
"filename": "pages",
"directory": true,
"contentType": "application/vnd.apple.pages",
"modifiedAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"lobId": null,
"ownerId": null,
"data": {},
"templateId": null,
"role": null,
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"description": null,
"assistantId": null,
"integrationId": null,
"remoteId": null,
"remoteUrl": null,
"size": null,
"indexed": false,
"indexingStartedAt": null,
"indexedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"messageId": null,
"chatId": null,
"expiresAt": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

Behavior:

  • Rejects sibling-name collisions with 409 Conflict (no silent overwrite — PUT /contents is the upsert endpoint)
  • Files get an empty content blob; reads of new files succeed and return zero bytes
  • parentId must reference an existing directory in the same library, or be null
  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt
  • Refused while a deploy is staged; see POST /files/_stage

Error Responses:

  • 404 Not Found - App doesn't exist
  • 403 Forbidden - User lacks write permission
  • 409 Conflict (name collision) - A file or directory with the same filename already exists under parentId
  • 409 Conflict (staged deploy) - A deploy is staged for this app, so the new file would be discarded when the staged build is promoted. Unlike the collision above, a different filename does not help: nothing can be created here until the deploy finishes or is discarded
  • 400 Bad Request - Missing/invalid filename, unknown parentId, or parentId references a non-directory

Example - Create a file at the root:

curl -X POST "${SERVER_URL}/api/apps/myapp/files" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"filename": "index.html"}'

Example - Create a directory under another directory:

curl -X POST "${SERVER_URL}/api/apps/myapp/files" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"filename": "components", "parentId": "src-dir-uuid", "directory": true}'

PUT /api/apps/{id}/files/{fileId}​

Rename and/or move a file or directory. Both filename and parentId are independently optional — pass either, both, or hit the no-op path by passing the current values.

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID
fileIdstringFile UUID (from the files list)

Request Body:

FieldTypeDescription
filenamestringNew basename (single segment, no slashes, max 255 chars)
parentIdstring | nullNew parent directory id; null means library root

At least one of filename or parentId must be present.

Rename or move a filePUT /api/apps/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002
filename and parentId are independently optional: rename in place, move, or both in one call. Moving a directory carries its children.
Request body
{
"filename": "home.html",
"parentId": "00000000-0000-4000-8000-000000000003"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/files/00000000-0000-4000-8000-000000000002"
}
},
"extension": "html",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"filename": "home.html",
"embedded": false,
"contentType": "text/html",
"lobId": "16400",
"role": null,
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"integrationId": null,
"directory": false,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": "00000000-0000-4000-8000-000000000003",
"libraryId": "00000000-0000-4000-8000-000000000004",
"remoteId": null,
"remoteUrl": null,
"size": "524288",
"modifiedAt": "2026-01-15T16:20:00.000Z",
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": null,
"templateId": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

Behavior:

  • Pure rename (filename only), pure move (parentId only), or both in one call
  • For directories, children's paths follow automatically (paths derive from the parentId chain)
  • Cycle prevention: cannot move a directory into itself or any of its descendants
  • Does NOT modify the app row; bumps the affected file's modifiedAt, surfaced via the app's derived contentModifiedAt
  • Refused while a deploy is staged; see POST /files/_stage

Error Responses:

  • 404 Not Found - App or file doesn't exist
  • 403 Forbidden - User lacks write permission
  • 409 Conflict (name collision) - A sibling with the new filename already exists under parentId
  • 409 Conflict (staged deploy) - A deploy is staged for this app, so the rename or move would be undone when the staged build is promoted. Unlike the collision above, a free filename does not help: nothing can be renamed or moved here until the deploy finishes or is discarded
  • 400 Bad Request - Invalid filename, unknown parentId, parentId references a non-directory, or attempted cycle

Example - Rename in place:

curl -X PUT "${SERVER_URL}/api/apps/myapp/files/${fileId}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"filename": "Header.tsx"}'

Example - Move to a different directory:

curl -X PUT "${SERVER_URL}/api/apps/myapp/files/${fileId}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"parentId": "components-dir-uuid"}'

POST /api/apps/{id}/_upload​

Upload a file using chunked multipart upload. This endpoint is designed for large file uploads from web browsers.

Authentication: Required

Permissions Required: app:write (Designer for Magic Reports, Admin for full Apps)

Path Parameters:

ParameterTypeDescription
idstringApp UUID or natural ID

Payload:

JSON. The bytes themselves are sent to the chunked upload flow first, which yields an uploadId; this route assembles that upload into the app's library.

FieldTypeRequiredDescription
uploadIdstringYesId returned by the chunked (Flow.js) upload
pathstringNoFile path within the library (e.g. assets/index.js). Parent directories are created as needed
stagingLibraryIdstringNoId returned by POST /files/_stage. Claims the staged deploy, which sends this upload into the staging library instead of the live one

Response:

Returns the created file, with a Location header pointing at it.

Use Case:

This endpoint is used by the file upload UI for:

  • Large binary files
  • Progress tracking
  • Resume capability

Along with PUT /contents/{path*}, it is one of the two write paths a staged deploy can use. The same claimed-redirect rule applies: while a deploy is staged, an upload presenting the matching stagingLibraryId lands in the staging library, and one presenting none (or a stale id) is refused with 409 Conflict.

Error Responses:

  • 404 Not Found - App doesn't exist or user lacks access
  • 403 Forbidden - User lacks write permission
  • 409 Conflict - A deploy is staged for this app and the request presented no stagingLibraryId, or one that is not the app's current staged upload

Common File Operations​

Create a Simple App​

// 1. Create the app
const app = await POST('/api/apps', {
type: 'report',
name: 'My Dashboard'
});

// 2. Add HTML file
await PUT(`/api/apps/${app.id}/contents/index.html`, {
content: '<!DOCTYPE html><html>...',
contentType: 'text/html'
});

// 3. Add CSS file
await PUT(`/api/apps/${app.id}/contents/styles.css`, {
content: 'body { margin: 0; }',
contentType: 'text/css'
});

// 4. Add JavaScript file
await PUT(`/api/apps/${app.id}/contents/app.js`, {
content: 'console.log("Ready");',
contentType: 'application/javascript'
});

Update a File​

// Read current content
const response = await GET('/api/apps/analytics:sales-dashboard/contents/index.html');
const currentHTML = await response.text();

// Modify it
const updatedHTML = currentHTML.replace('Old Title', 'New Title');

// Write back
await PUT('/api/apps/analytics:sales-dashboard/contents/index.html', {
content: updatedHTML,
contentType: 'text/html'
});

Bulk Replace Across File​

// Replace all occurrences of a string
await PATCH('/api/apps/analytics:sales-dashboard/contents/index.html', {
operation: 'replace',
search: 'oldClassName',
replacement: 'newClassName',
replaceAll: true
});

List and Delete Old Files​

// Get all files
const files = await GET('/api/apps/analytics:sales-dashboard/files');

// Find old temp files
const tempFiles = files.filter(f => f.filename.startsWith('temp-'));

// Delete them
for (const file of tempFiles) {
await DELETE(`/api/apps/analytics:sales-dashboard/files/${file.id}`);
}