v1 API

HTTP API

Media

A media field gives you an asset id, not a URL. Two endpoints turn that id into something you can render: one redirects to the file, the other describes it.

Ids, not URLs

When an entry has a media field, its value is an opaque asset id:

json
"cover_image": "0c4d9e11-7b2a-4c38-9de5-1f60a7b8c9d0"

Putting that straight into an <img src> renders nothing. Build the media URL from it instead. The pattern is stable and safe to construct by hand:

html
<!-- A media field's value is an id. This URL redirects to the file. -->
<img src="https://acme.sedgemark.app/api/v1/media/0c4d9e11-7b2a-4c38-9de5-1f60a7b8c9d0" alt="" />
Why an id and not a stored URL

The underlying storage location is an implementation detail that can change: a different bucket, a different provider, a CDN in front. Storing ids and resolving them at read time means none of that is a content migration. It is also what lets the file itself be served from a short-lived signed URL rather than a permanently public one.

Fetching the file

GET /api/v1/media/{id}

No authentication.

This does not return the file. It returns a 302 redirect to a signed storage URL valid for about an hour. Browsers, image tags, CSS url() and fetch all follow it automatically, so in practice you use it exactly as if it were the file.

StatusMeaning
302Redirect to the signed URL. Cache-Control: public, max-age=3000, s-maxage=3000. The shared-cache directive matters if you put a CDN in front.
400{ "error": "Invalid ID" }: the id is not a well-formed UUID.
404{ "error": "Not found" }: no asset with that id in this workspace.
Cache the redirect, never its destination

The signed URL you are redirected to expires in about an hour. The /api/v1/media/{id} URL does not expire: it is stable for the life of the asset. Store and share that one. If you capture the resolved storage URL into a build artifact or a database, your images will work in testing and start failing roughly an hour later.

The max-age above is deliberately a little under the signing window, so a browser refetches the redirect before what it points at goes stale.

A one-line helper is usually all you need:

javascript
const SEDGEMARK_BASE_URL = 'https://acme.sedgemark.app'

function resolveMedia(id) {
  return `${SEDGEMARK_BASE_URL}/api/v1/media/${id}`
}

// post.cover_image is an id, or null when nothing was picked.
const coverUrl = post.cover_image ? resolveMedia(post.cover_image) : null

The generated TypeScript client exports a resolveMedia that does exactly this. See TypeScript client.

Reading an asset’s metadata

GET /api/v1/media/{id}/meta

No authentication.

Returns JSON rather than a redirect. Useful when you need the intrinsic dimensions before the image loads: reserving layout space, choosing an aspect ratio, or picking between renditions.

json
{
  "width": 1600,
  "height": 900,
  "mimeType": "image/jpeg",
  "size": 284713,
  "filename": "harbour-at-dusk.jpg",
  "alt_text": "Fishing boats moored at sunset"
}
KeyTypeNotes
widthnumber | nullPixel width. Null for anything that is not a raster image, and for assets uploaded before dimension probing existed.
heightnumber | nullPixel height. Same caveat as width.
mimeTypestringFor example image/png.
sizenumberBytes.
filenamestringThe original upload filename.
alt_textstring | nullAlt text set in the dashboard, when it was set.

The mixed casing (mimeType in camel case beside alt_text in snake case) is the actual contract, not a typo in these docs.

javascript
const res = await fetch(
  `https://acme.sedgemark.app/api/v1/media/${id}/meta`
)
const meta = await res.json()

// width/height are null for anything that is not a raster image.
if (meta.width && meta.height) {
  // Reserve the box before the image loads, so the page does not jump.
}

Metadata does not change after upload, so this response is cacheable for a day ( Cache-Control: public, max-age=86400). The same 400 and 404 responses apply as above; see Errors & rate limits for the rest.

/meta is not callable from a browser

Unlike the redirect above, this endpoint returns JSON and sends no CORS headers, so a cross-origin fetch is blocked, even though no key is involved. Call it from a build step or a server. Loading the image itself as an <img src> is unaffected, because that is not a CORS-governed request.

Media is always public

Content visibility does not extend to media

Both endpoints on this page are unauthenticated for every asset, including assets referenced only from private collections. Media is decoupled from collection visibility and there is no per-asset access control.

In practice this means: anyone holding an asset id can fetch the file. Ids are UUIDs and are not enumerable, but they are not a permission either. Do not upload files whose contents would be harmful to disclose and rely on a private collection to protect them.

This is a deliberate trade. Images are the thing a static site needs to reference from markup that never carries a credential, and making them public is what lets a <img src> point straight at Sedgemark with no key and no proxy in between.

What Sedgemark does not do

  • No upload over this API. Assets are uploaded from the dashboard’s media library, which enforces its own size and file-type limits and shows them to you there. /api/v1 reads media; it does not accept it.
  • No on-demand transforms. There is no ?w=, ?h= or ?fit=. You get the file as uploaded. Resize at build time, or put an image CDN in front of the media URL.
  • No format negotiation. Upload the format you want served.
  • Dimensions are best-effort, not guaranteed. They are probed at upload for raster images; a file whose dimensions could not be read still uploads successfully with null for both. Always branch on null rather than assuming a number.