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 S3 PUT
that you perform yourself, and S3 tells DFOS when it lands. 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 by
S3. 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 S3. 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: S3 notifies DFOS
asynchronously, usually within seconds, and there is nothing to call to hurry it.
uploaded is the only signal. Until it flips, url and contentLength are
absent — a pending row has no bytes, so it has no address and no measured
size.
If uploaded stays false long after a successful PUT, the bytes never
reached S3 (an egress-restricted client) 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:
| Kind | Served as | url |
|---|---|---|
| Images | A public CDN URL | Permanent, unsigned. No urlExpiresAt. |
| Audio, video, and every other file type | A short-lived signed S3 URL | Expires — urlExpiresAt says when. Never persist it. |
That split is why a paid or gated file cannot leak: a private object has no address that works without a fresh signature. 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: ",
"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
| Status | Meaning |
|---|---|
400 | size missing, over its kind's cap, or a body that fails validation. |
401 | The proof was missing, malformed, or stale — and on the mint only, carried no jti. The status check needs none. |
403 | On 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. |
404 | On 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. |
409 | On 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 ever attached is garbage. DFOS reclaims one 30 days after it was minted — long enough to cover any draft, retry, or client crash. 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.
Nothing you can do shortens or extends that window, and there is no delete endpoint: removing the last reference is what makes a file collectable.
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.