File API workflows
Resolve these routes against the workspace API URL supplied by your
administrator. Regenerate your client from that installation’s
/openapi/moltaro-public-v1.json after upgrading: a more precise schema can
change generated types even when the JSON and SDK signatures stay unchanged.
Choose the destination and source
Section titled “Choose the destination and source”The catalogue is a user-visible folder structure. Database, LocalFileSystem and AmazonS3 are storage providers. Uploading from a computer does not select the LocalFileSystem provider.
| Destination | Computer / stream | Existing catalogue file | Completion |
|---|---|---|---|
| Catalogue folder | Managed upload to an authorized explicit folder; in C#, IFileCatalogService.UpsertStreamAsync | Select/read the existing reference; moving/copying is a separate supported operation | Verified file published in that folder |
| Scalar File | Managed upload with FieldTarget; in C#, IFileFieldService.UploadStreamAsync | Assign the permitted existing reference ID | File available, Entity save committed, authorized field readback |
| File inside Table | Same upload with the child File field ID | Assign the reference to the intended row’s cell | Same checks, correct row identity, other cells/rows preserved |
| Collaboration attachment | Prepare a small file or use a deferred managed session, then Send | Attach from catalogue or link an existing permitted file through Send | Publication/link committed; authorized attachment read and download |
The public Runtime API includes upload sessions, Entity mutation/file reads,
attachments, Collaboration, Catalogue Explorer listing, folder upload and generic
file reads/downloads. Use the installed Runtime OpenAPI to generate an external
client; older installations may need an update for complete publication.
Native Select / From catalogue uses these authorized catalogue operations.
IFileCatalogService remains the separate C# integration for in-project logic.
Managed POST files/uploads requires an explicitly authorized destination folder;
discover/select that destination before starting the upload.
Disabling the catalogue prevents catalogue selection/use. It does not disable supported direct field or attachment uploads. Destination permission, file-type policy and size limits still apply. A file ID or matching checksum grants no permission to read, reuse, change ownership or delete a file.
Managed upload
Section titled “Managed upload”- Read
GET /api/workspace/files/uploads/capabilities. ApplyFileTypeRules, limits and capabilities. Preserve oneIdempotencyKeyand the exact metadata for the logical upload. POST /api/workspace/files/uploadswith the destination and source facts. Keep itsUploadSessionIdandRowVersion. For a field, useFieldTargetrather than a folder/attachment owner. An existing record usesInstanceId; a new record uses its originalCreateOperationKeyand actualDraftFields.- When
ManagedMultipartEnabled, authorize exact parts withPOST .../{uploadSessionId}/parts/authorize, PUT each part to its returned URL with the returned headers, reconcileGET .../{uploadSessionId}/partsafter interruption, thenPOST .../{uploadSessionId}/completewith the currentRowVersion. Provider URLs are short-lived secrets; do not log them. - For a direct File field when
LegacyStreamEnabled, PUT the raw bytes asapplication/octet-streamto.../{uploadSessionId}/content. This endpoint is specific to direct fields. Small attachment preparation instead usesmultipart/form-dataatcollaboration/files/prepare. The legacy immediateattachmentsupload is a separate multipart operation; it is not a universal transport for every provider or resource owner. - Poll
GET /api/workspace/files/uploads/{uploadSessionId}. Receiving an ID, transferring every byte or getting an accepted complete response is not verification success. For automatic publication require Status 6 (Available) and a non-nullPublishedFileReferenceId. Then perform the destination’s business action and read it back.
REST session states are numeric: Created=0, Uploading=1, Completing=2,
PendingVerification=3, Publishing=4, PublishBlocked=5, Available=6,
Aborting=7, Aborted=8, Expired=9, Rejected=10, Failed=11,
ReadyForPublication=12. The OpenAPI enum descriptions describe their meaning.
For ordinary waiting states, poll with a bounded delay (for example one second,
backing off to five seconds) and an overall deadline; honor HTTP Retry-After
when supplied. A delay is never evidence of readiness. PublishBlocked needs
resolution/reconciliation, not publication by the client. Stop on terminal
refusal or an unknown state; preserve the key/session and failure details.
The C# transfer result has its own State and IsTerminal. It is not the REST
session model. See the executable C# helper in
File fields and attachments.
Publish or save
Section titled “Publish or save”For Collaboration, PublicationMode=1 (DeferredUntilSend) keeps a verified
managed file at ReadyForPublication, unpublished. Small prepared files use
POST /api/workspace/collaboration/files/preparations/status: Preparing=1,
Ready=2, Rejected=3, Expired=4. Honor each item’s RetryAfterMilliseconds.
Ready permits Send; it does not mean that the normal attachment list includes
the file. Send ready IDs in PreparedFileReferenceIds or ready session IDs in
DeferredUploadSessionIds. A file-only Send omits text and creates no empty
comment. Keep one Send OperationKey and the exact draft for an uncertain-answer
retry; check Replayed, then perform authorized readback/download.
An available File-field upload still has not saved the Entity. Save the scalar reference or the correct Table row with an independent Entity mutation key and the appropriate row version. A new record must retain its original create key. Preserve existing row IDs and unrelated values; do not manufacture a new row on each retry. A failed save leaves the assignment incomplete. A safe mutation receipt acknowledges a commit without granting access to the resulting value.
Catalogue assignment to a File field preserves the existing FileReference ID
and does not create an ordinary attachment or another catalogue copy.
attachments/from-catalog creates a new attachment reference sharing the
existing descriptor/body; retain the returned attachment ID. Collaboration
ExistingFileReferenceIds links an existing file already authorized in that
Collaboration context without copying or republishing it. A raw catalogue ID
does not satisfy that check: attach from catalogue first and use the returned
attachment ID when linking a message. These are distinct operations with independent source-read
and destination-write checks. Module-owned and Board content must use its owning
Collaboration/Board operation; generic attachment upload is not an authorization
workaround.
Read mutation results and download
Section titled “Read mutation results and download”For an Entity attachment mutation, inspect Data.Kind in the response envelope:
AuthorizedAttachment:Data.Datacontains file reference ID, file name, content type, size and row version. Preparation still needs readiness and Send.Receipt: aResourceMutationReceiptacknowledges the mutation with its operation key, completion instant, caller-known identity, audit correlation and replay flag.CanViewResultis false. There is no file payload to display.
The legacy attachment routes can also answer a non-Entity FileUploadResult,
FileReference, or a deleted ID string depending on the operation. There is no
top-level discriminator on ApiResponse. Errors can have Data: null. Check
HTTP status and Success before accessing data; an unknown variant must stop
further publication without guessing or deleting anything.
A download can stream binary content or return a JSON download authorization
marked by X-Moltaro-Download-Authorization: 1. In the latter case follow
the exact short-lived DownloadUrl only under the returned origin/expiry
contract; do not send the workspace bearer token to storage. Never log the URL.
An empty Forbidden or infrastructure Problem Details response is not an
ApiResponse success. Use authorized Entity file reads for structured File
values and the resource owner’s attachment download for attachments.
For scalar and Table File values, the record-authorized route is
GET /api/workspace/entity/{entityIdOrKey}/instances/{instanceId}/files/{fileId}/download.
It has the same binary/authorization alternatives and checks the readable
record and field; a generic attachment URL is not a replacement for that check.
Recovery and cancellation
Section titled “Recovery and cancellation”Upload idempotency key, physical session ID, Send OperationKey and Entity
mutation key describe different boundaries. Reconcile the existing session or
replay the unchanged operation after a lost answer. The Entity receipt’s
seven-day replay rule does not define upload retention: a durable logical upload
can retain its published result, and an exact retry of an expired attempt can
create one successor after current policy checks.
Cancel a small prepared file through collaboration/files/cancel; abort an
unfinished managed session through files/uploads/{uploadSessionId}/abort with
its current row version. The creator’s supported abort after losing destination
access does not grant status, file-read or publication access. Removing an
attachment, unlinking it from a comment, clearing a field and aborting a session
are different actions. Never delete a shared catalogue file to compensate for a
failed save or for removing one reference.
Existing LegacyUninspected values retain the installed legacy-read policy.
Do not assign that state to a new upload or use it to bypass inspection. Archived
or merged-source records can remain readable without permitting file changes.
If access disappears between prepare and publication, accept the refusal or
safe receipt without exposing old cached file names or a hidden destination.
Typed REST examples
Section titled “Typed REST examples”Generate schema.d.ts with openapi-typescript from the installed public
OpenAPI. The following example is compiled against a fresh host export and its
read functions are exercised with host HTTP responses. Supply your existing
authenticated workspace fetch function; keep credentials out of URLs.
Persist operation keys before the first request. The example advances one step,
so your caller retains the deadline, delay and reconciliation described above.
import type { components, paths } from "./schema";type S = components["schemas"];type WorkspaceFetch = (path: string, init?: RequestInit) => Promise<Response>;type PrepareAnswer = paths["/api/workspace/collaboration/files/prepare"]["post"]["responses"][200]["content"]["application/json"];
// POST this JSON to /api/workspace/files/uploads, then use the selected transport.export function fieldUpload( target: S["MoltaroFileFieldTarget"], fileId: string, uploadKey: string, name: string, mime: string, bytes: number, sha256: string,): S["CreateManagedFileUploadRequest"] { return { FieldTarget: target, FileReferenceId: fileId, IdempotencyKey: uploadKey, RequestedFileName: name, DeclaredMimeType: mime, DeclaredSizeBytes: bytes, ClientDeclaredSha256: sha256, PublicationMode: 0 };}
// For a catalogue upload use an explicitly authorized folder and no FieldTarget.export function catalogueUpload(folderId: string, uploadKey: string): S["CreateManagedFileUploadRequest"] { return { TargetFolderId: folderId, IdempotencyKey: uploadKey, RequestedFileName: "evidence.txt", DeclaredMimeType: "text/plain", DeclaredSizeBytes: 4 };}
export async function prepareSmall( fetchWorkspace: WorkspaceFetch, owner: S["MoltaroResourceIdentity"], operationKey: string, file: File,) { if (!owner.ModuleKey || !owner.ResourceKind || !owner.ContextResourceId || !owner.ResourceObjectId) throw new Error("A complete canonical owner is required"); const form = new FormData(); form.set("File", file); form.set("OperationKey", operationKey); form.set("OwnerModuleKey", owner.ModuleKey); form.set("OwnerResourceKind", owner.ResourceKind); form.set("OwnerContextResourceId", owner.ContextResourceId); form.set("OwnerResourceObjectId", owner.ResourceObjectId); // Let fetch set the multipart boundary. const response = await fetchWorkspace("/api/workspace/collaboration/files/prepare", { method: "POST", body: form }); if (!response.ok) throw new Error(`Preparation refused: ${response.status}`); const answer: PrepareAnswer = await response.json(); if (!answer.Success || !answer.Data) throw new Error("Preparation failed"); switch (answer.Data.Kind) { case "Receipt": return { state: "receipt", operationKey: answer.Data.OperationKey }; case "AuthorizedAttachment": return { state: "check-readiness", fileId: answer.Data.Data.FileReferenceId }; default: throw new Error("Unknown preparation result"); }}
export async function inspectUpload(fetchWorkspace: WorkspaceFetch, sessionId: string) { const response = await fetchWorkspace(`/api/workspace/files/uploads/${encodeURIComponent(sessionId)}`); if (!response.ok) throw new Error(`Status refused: ${response.status}`); const answer: S["ManagedFileUploadSessionResponse"] = await response.json(); if (!answer.Success || !answer.Data) throw new Error("Status unavailable"); const data = answer.Data; switch (data.Status) { case 6: if (!data.PublishedFileReferenceId) throw new Error("Available without a published ID"); return { state: "available", fileId: data.PublishedFileReferenceId }; case 12: return { state: "ready-for-send" }; case 0: case 1: case 2: case 3: case 4: case 7: return { state: "wait" }; case 5: throw new Error("Publication blocked; reconcile the destination"); case 8: case 9: case 10: case 11: throw new Error(data.FailureCode ?? "Upload refused"); default: throw new Error("Unknown upload state"); }}
// path is the record-authorized File download URL above. This decodes one answer;// following an authorization is a separate storage request without workspace credentials.export async function readFileDownload(fetchWorkspace: WorkspaceFetch, path: string) { const response = await fetchWorkspace(path); if (!response.ok) throw new Error(`Download refused: ${response.status}`); const mode = response.headers.get("X-Moltaro-Download-Authorization"); if (mode === null) return { state: "bytes", bytes: new Uint8Array(await response.arrayBuffer()) }; if (mode !== "1") throw new Error("Unknown download authorization mode"); if (!response.headers.get("content-type")?.includes("application/json")) throw new Error("Download authorization must be JSON"); const answer: paths["/api/workspace/entity/{entityIdOrKey}/instances/{instanceId}/files/{fileId}/download"]["get"]["responses"][200]["content"]["application/json"] = await response.json(); if (!answer.Success || !answer.Data) throw new Error("Download authorization unavailable"); const authorization = answer.Data; const url = new URL(authorization.DownloadUrl); const expected = new URL(authorization.DataPlaneOrigin); if (url.protocol !== "https:" || url.origin !== expected.origin || expected.pathname !== "/" || expected.search || expected.hash || url.username || url.password) throw new Error("Unexpected storage origin"); return { state: "authorization", authorization };}
// POST to /api/workspace/attachments/from-catalog. Use its returned attachment ID.export function attachCatalogue(owner: S["MoltaroResourceIdentity"], key: string, sourceId: string): S["AttachFileFromCatalogRequest"] { return { Owner: owner, OperationKey: key, FileId: sourceId };}
// POST to /api/workspace/collaboration/send only after the selected files are ready.export function sendReady(owner: S["MoltaroResourceIdentity"], key: string, preparedIds: string[], sessionIds: string[]): S["CollaborationSendRequest"] { return { Owner: owner, OperationKey: key, PreparedFileReferenceIds: preparedIds, DeferredUploadSessionIds: sessionIds }; // File-only publication; no empty comment.}
// A permitted catalogue reference goes straight into Patch: do not upload it again.// Send this through the published Entity update mutation, then authorized readback.export function assignFile(definition: string, record: string, rowVersion: string, key: string, fieldKey: string, referenceId: string): S["EntityUpdateCommandRequest"] { return { EntityDefinitionKey: definition, EntityInstanceId: record, RowVersion: rowVersion, OperationKey: key, Patch: { [fieldKey]: referenceId } };}For an existing scalar field, target contains EntityDefinitionIdOrKey,
FieldId and InstanceId. For a new record omit InstanceId, supply
CreateOperationKey and its DraftFields, and later create with that same key.
For Table use the child File FieldId; save a patch of the intended table rows,
preserving each existing Id, its other cell values and all retained rows.
The published Entity mutation’s Patch is keyed by field keys, whereas upload
FieldTarget.FieldId is the immutable definition ID.
For S3, catalogueUpload is followed by authorize/PUT/complete/status; the
catalogue SDK supplies the existing stream adapter for Database and Local
storage. If checksum is required, supply the real source SHA-256 before POST.
For direct field stream transport, PUT the original File body to the session’s
content route with Content-Type: application/octet-stream, then call
inspectUpload. A returned available value still needs the Entity save.
For deferred large attachments construct the same upload request with the
canonical owner coordinates and PublicationMode: 1, then pass only verified
session IDs to sendReady. Small files instead use prepareSmall, preparation
status and verified prepared IDs. A receipt does not supply a file ID for Send.
To link an already permitted attachment to a new message, send its ID in
ExistingFileReferenceIds with CommentText; that operation creates a link,
not another upload. Do not send empty file-only requests with only existing IDs.
readFileDownload does not follow or log the returned URL. Before the separate
storage request, reject an expired ExpiresAtUtc; never forward workspace
credentials. Decide how to save the byte result using your application’s file
handling policy.