Skip to main content

Media

Two endpoints put a file on DFOS:

  • POST /v1/spaces/{space}/media — mint an upload.
  • GET /v1/spaces/{space}/media/{mediaId} — check whether it finished.

The bytes never go through this API. The mint hands back a presigned upload URL that you PUT the bytes to yourself. Both endpoints are gated by write:posts or write:comments — an upload is the first step of a post or a comment, not an authority of its own.

The mint is signed like a write, with a jti; the status check is signed like a read, without one. That is the ordinary rule for the method, not a special case: createApiAuthFetch attaches a jti to everything except GET, HEAD, and OPTIONS, so a normal signed client polls correctly with no extra configuration. See Conventions § Writing.

Four steps​

1. Mint​

curl -X POST 'https://api.dfos.com/v1/spaces/home/media' \
-H 'Authorization: DFOS <request-proof>' \
-H 'X-Credential: <credential>' \
-H 'Content-Type: application/json' \
--data '{
"filename": "studio-floor.jpg",
"contentType": "image/jpeg",
"size": 812446
}'

201 Created:

{
"media": {
"id": "media_2t9crk4hf7vz3ea8dn6r2c",
"filename": "studio-floor.jpg",
"contentType": "image/jpeg",
"uploaded": false
},
"upload": {
"url": "https://dfos-media.s3.us-east-1.amazonaws.com/media/public/2t9crk4hf7vz3ea8dn6r2c-studio-floor.jpg?X-Amz-Algorithm=…",
"method": "PUT",
"headers": {
"Content-Type": "image/jpeg",
"Content-Disposition": "inline; filename=\"studio-floor.jpg\""
},
"expiresAt": "2026-09-08T17:20:00.000Z"
}
}

size is required and must be the file's exact byte length: it is signed into the PUT as Content-Length, so a body of any other length is rejected. Caps are per kind — images 10MB, audio, video, and everything else 4GB — and a size over the cap for its contentType is a 400.

2. Send the bytes​

curl -X PUT --upload-file studio-floor.jpg \
-H 'Content-Type: image/jpeg' \
-H 'Content-Disposition: inline; filename="studio-floor.jpg"' \
'https://dfos-media.s3.us-east-1.amazonaws.com/media/public/…'

Send both headers exactly as returned. They are inside the signature; altering, reordering the case of, or omitting either gives you 403 SignatureDoesNotMatch from the upload URL. Send no Authorization header — the URL is the credential. The signature dies at expiresAt (one hour); after that, mint again. There is no way to extend one.

3. Poll until it finalized​

curl 'https://api.dfos.com/v1/spaces/home/media/media_2t9crk4hf7vz3ea8dn6r2c' \
-H 'Authorization: DFOS <request-proof>' \
-H 'X-Credential: <credential>'
{
"id": "media_2t9crk4hf7vz3ea8dn6r2c",
"filename": "studio-floor.jpg",
"url": "https://dfos.imgix.net/media/public/2t9crk4hf7vz3ea8dn6r2c-studio-floor.jpg",
"contentType": "image/jpeg",
"contentLength": 812446,
"uploaded": true
}

A successful PUT returns before DFOS has recorded it, usually by a few seconds, and there is nothing to call to hurry it. uploaded is the only signal. Until it flips, url and contentLength are absent.

If uploaded stays false long after a successful PUT, the bytes never arrived or the headers did not match.

4. Attach it​

{
"topic": "topic_6c2efd472dvt8rf9k4ftcc",
"body": "From the studio.",
"attachments": ["media_2t9crk4hf7vz3ea8dn6r2c"]
}

attachments is an ordered array of media ids on post create and edit and on comment create and edit. Attaching before uploaded is true is allowed — the reference is valid either way — but the attachment will not render until the bytes land, which is what the poll is for.

Both posts and comments read the media back as an attachments array of the same shape, so a comment carrying only a file is legible: body is null and attachments holds it. Comment attachments are never folded — a caller who can read the thread gets its files in full.

Public and private​

The contentType decides how the object is served, and you do not get a say:

KindServed asurl
ImagesA public URLPermanent, unsigned. No urlExpiresAt.
Audio, video, and every other file typeA short-lived signed URLExpires — urlExpiresAt says when. Never persist it.

See Compatibility → Media URLs.

Inline images in a post body​

A long post (one with a title) can place an attached image inside its markdown with an attachment:// token:

{
"topic": "topic_6c2efd472dvt8rf9k4ftcc",
"title": "From the studio",
"body": "Here is the floor: ![shot](attachment://media_2t9crk4hf7vz3ea8dn6r2c)",
"attachments": ["media_2t9crk4hf7vz3ea8dn6r2c"]
}

The token resolves to a bodyMedia entry on read, matched by id. Only media you uploaded resolves; a token to anything else is a 400.

Short posts and comments refuse inline images — carry them as attachments instead. Only a DFOS media id works; an external image URL is a 400.

Refusals​

StatusMeaning
400size missing, over its kind's cap, or a body that fails validation.
401The proof was missing, malformed, or stale — and on the mint only, carried no jti. The status check needs none.
403On a space with a public profile: the user is not a member of it. On the mint, also: the grant carries neither write:posts nor write:comments here.
404On the status check: the media does not exist, you did not upload it, it has been reclaimed, or your grant does not cover this space. All four are the same answer. Also, on both routes: a private space you are not a member of.
409On the mint only. This jti was already accepted; re-read before retrying, with a new jti.

Attaching media you did not upload gives you the same 404 on the write, in the form Media not found: media_….

Reclamation​

An upload nobody attaches is removed 30 days after it was minted. Once media is attached to a live post or comment it is kept for as long as that content exists, including while the content sits in the trash.

The 30-day window cannot be shortened or extended, and there is no delete endpoint.


See the interactive reference for the exact schema of every field, and Post a comment as a user for the write flow this plugs into.