Skip to main content

Files

A file is not an object of its own. It is the value of a field of type FILE or IMAGE on an object, just like a date or an amount is the value of a DATE or CURRENCY field. There is no /files collection to create, list or delete files in: you upload the bytes, and then store the result in a field of an object with an ordinary create or patch. Removing the file is clearing that field.

FILE and IMAGE fields work exactly the same way: everything on this page applies to both.

The bytes themselves never go through the data API. Uploads and downloads use short-lived signed URLs under /binaries/.

The examples use commons.document: a document in a dossier, whose required file field holds the file.

Uploading a file​

Getting a file onto an object takes three requests: ask for an upload URL, upload the bytes to it, then store the result in the FILE field.

1. Request an upload URL​

GET /data/{tenant}/upload

An ordinary, authenticated data API request. The response is a signed URL, returned both as the Location header and as the JSON body:

"https://api.mosterd.com/binaries/eyJhbGciOiJFUzI1NiJ9…"

The token in the URL is valid for 2 minutes and for one upload. It already identifies the tenant.

2. Upload the file — without credentials​

POST /binaries/{token}
Content-Type: multipart/form-data

Send the file as a multipart part named file:

curl -X POST "https://api.mosterd.com/binaries/eyJhbGciOiJFUzI1NiJ9…" \
-F "file=@signed-contract.pdf"
No credentials on /binaries/

Do not send an Authorization header or an X-API-Key header with requests to /binaries/. The signed token in the URL is the only credential this request needs or accepts.

This matters most for clients that attach credentials automatically. If your HTTP client or interceptor adds a bearer token to every request to the API host — for example the Auth0 SDK's httpInterceptor.allowedList — leave /binaries/ out of it. It also means a browser can upload straight to the signed URL, without the page ever handing it a token or key.

The response is a JSON array with one entry per uploaded file (currently always exactly one):

[
{ "href": "/binaries/3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fileName": "signed-contract.pdf",
"contentType": "application/pdf",
"size": 48213 }
]

fileName, contentType and size are determined by the server from the upload itself, never taken from what the client claims. The file is now stored but unclaimed: it belongs to no object yet, and nobody can reach it through the data API.

3. Store it in a field​

Put the object from step 2 — unchanged, not just its href — in the FILE field of a create or patch request. To create a new document in a dossier:

POST /data/{tenant}/documents
{
"dossier": "/data/2f9c6e1a-7b34-4d58-9c21-0a5e8f1b2c3d/dossiers/7c2e5a91-3d4b-4f86-a0e1-9b8d6c3f2e17",
"file": {
"href": "/binaries/3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fileName": "signed-contract.pdf",
"contentType": "application/pdf",
"size": 48213
}
}

The same body without dossier can be posted to the dossier's related list, POST /data/{tenant}/dossiers/{dossierId}/documents — see Create related. To replace the file of an existing document, PATCH /data/{tenant}/documents/{id} with only the file property.

The server looks up the upload by href and checks fileName, contentType and size against what it recorded in step 2. A mismatch is rejected with 400 Bad Request. When they match, the file becomes the value of the field; from then on, who may read or change it follows the object's own access control, like any other field.

Multiple files. A FILE field with multiple: true takes an array of these objects. To add a file, send the files already in the field plus the new upload; to remove one, leave it out of the array. A file that is already in the field may be sent back as just its href string — it is matched by the file id at the end of the href and kept as it is.

Removing a file is clearing the field: { "file": null } — or deleting the object that holds it. For commons.document, whose file field is required, clearing it is rejected; delete the document instead.

Reading the field back​

The upload href (/binaries/{fileId}) is only used to claim the file in step 3. Once the file is stored, reading the object returns the field with a download link instead, which contains the id of the object and the id of the file:

"file": {
"contentType": "application/pdf",
"fileName": "signed-contract.pdf",
"size": 48213,
"href": "/data/2f9c6e1a-7b34-4d58-9c21-0a5e8f1b2c3d/download/c4a9e2f7-1b6d-4e38-9f05-8a3d7b2c6e91/3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

Downloading a file​

Downloading takes two requests, mirroring the upload.

1. Request a download URL​

GET /data/{tenant}/download/{objectId}/{fileId}

The path names both the object that holds the file (objectId) and the file itself (fileId). You don't have to build it: it is exactly the href of the field as the object returns it, so call that href (relative to the API base URL) as it is.

This is an ordinary, authenticated request, and it follows the access control of the object: the response is 404 Not Found, exactly as if the file did not exist, when

  • the object does not exist, or the caller may not read it, or
  • the file is not in one of that object's FILE or IMAGE fields — including an upload that was never stored in a field.

Otherwise the response is a signed URL, valid for 2 minutes:

"https://api.mosterd.com/binaries/eyJhbGciOiJFUzI1NiJ9…"

2. Fetch the file — without credentials​

curl -o signed-contract.pdf "https://api.mosterd.com/binaries/eyJhbGciOiJFUzI1NiJ9…"

As with the upload, send no Authorization or X-API-Key header: the token in the URL is the credential. That is also what lets a browser use the signed URL directly as a link, an <iframe> source or an <img> source, as described below.

TODO

Interview needed: how this relates to the separate GET /assets/{tenantId}/{fileId} endpoint, and the other document dimensions besides the plain uploaded file (ASSETS, IMPORT, and the on-demand LAYOUT/TEXT/INVOICE conversions mentioned under Inline preview vs. download).

Inline preview vs. download​

The returned signed URI resolves with a Content-Disposition that depends on the binary's content type:

Content typeContent-DispositionBehaviour
application/pdfinlineBrowsers render the file in place instead of downloading it — the URI can be used directly as an <iframe> source.
image/*inlineThe URI can be used directly as an <img> source.
Everything elseattachmentBrowsers prompt a download.

For inline content types, the response also omits the X-Frame-Options: DENY header that is otherwise set, since inline previews are commonly embedded in a frame within the current page. This is automatic and content-type-driven — there is no separate parameter or endpoint to request one disposition over the other.

The signed URI is time-limited. A preview kept open longer than the URI's expiry (for example, an overlay left open) needs to request a fresh download URI rather than reusing the original one.