Skip to content

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.

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.

DestinationComputer / streamExisting catalogue fileCompletion
Catalogue folderManaged upload to an authorized explicit folder; in C#, IFileCatalogService.UpsertStreamAsyncSelect/read the existing reference; moving/copying is a separate supported operationVerified file published in that folder
Scalar FileManaged upload with FieldTarget; in C#, IFileFieldService.UploadStreamAsyncAssign the permitted existing reference IDFile available, Entity save committed, authorized field readback
File inside TableSame upload with the child File field IDAssign the reference to the intended row’s cellSame checks, correct row identity, other cells/rows preserved
Collaboration attachmentPrepare a small file or use a deferred managed session, then SendAttach from catalogue or link an existing permitted file through SendPublication/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.

  1. Read GET /api/workspace/files/uploads/capabilities. Apply FileTypeRules, limits and capabilities. Preserve one IdempotencyKey and the exact metadata for the logical upload.
  2. POST /api/workspace/files/uploads with the destination and source facts. Keep its UploadSessionId and RowVersion. For a field, use FieldTarget rather than a folder/attachment owner. An existing record uses InstanceId; a new record uses its original CreateOperationKey and actual DraftFields.
  3. When ManagedMultipartEnabled, authorize exact parts with POST .../{uploadSessionId}/parts/authorize, PUT each part to its returned URL with the returned headers, reconcile GET .../{uploadSessionId}/parts after interruption, then POST .../{uploadSessionId}/complete with the current RowVersion. Provider URLs are short-lived secrets; do not log them.
  4. For a direct File field when LegacyStreamEnabled, PUT the raw bytes as application/octet-stream to .../{uploadSessionId}/content. This endpoint is specific to direct fields. Small attachment preparation instead uses multipart/form-data at collaboration/files/prepare. The legacy immediate attachments upload is a separate multipart operation; it is not a universal transport for every provider or resource owner.
  5. 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-null PublishedFileReferenceId. 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.

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.

For an Entity attachment mutation, inspect Data.Kind in the response envelope:

  • AuthorizedAttachment: Data.Data contains file reference ID, file name, content type, size and row version. Preparation still needs readiness and Send.
  • Receipt: a ResourceMutationReceipt acknowledges the mutation with its operation key, completion instant, caller-known identity, audit correlation and replay flag. CanViewResult is 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.

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.

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.