Documentation menu

media

Uploading an image

Three requests. Reserve an upload, PUT the bytes straight to storage, then register the asset. The bytes never pass through the API.

Why three steps#

A presigned upload keeps image bytes off the API path, so a large file does not occupy a request slot and a slow connection does not hold one open. It also means you never name a storage location. The server chooses one inside your Site's namespace, and there is no request field anywhere that could point an upload somewhere else.

  1. 1

    Reserve

    POST /media/upload-url with the filename and the type you believe you are uploading. You get a short lived presigned URL and an upload_id.

  2. 2

    Upload

    PUT the raw bytes to that URL. Send no authorization header: the signature inside the URL is the credential.

  3. 3

    Register

    POST /media with the upload_id. Only now is the file part of your library.

The flow#

1. Reserve an upload

returns 201
curl
curl -X POST https://api.writavo.com/v1/media/upload-url \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: b6f4b0e2-0000-4000-8000-0000000000aa" \
  -d '{
  "file_name": "hero.png",
  "content_type": "image/png",
  "size_bytes": 68
}'

You never name a storage location. The server chooses one inside your Site's namespace.

2. PUT the bytes

returns 200
curl
curl -X PUT https://uploads.example.com/presigned/EXAMPLE \
  -H "Content-Type: image/png" \
  --data-binary @hero.png

Send no authorization header. The signature inside the URL is the credential, and it expires.

3. Register the asset

returns 201
curl
curl -X POST https://api.writavo.com/v1/media \
  -H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: b6f4b0e2-0000-4000-8000-0000000000aa" \
  -d '{
  "upload_id": "3f1b0c7a-0000-4000-8000-000000000006",
  "alt_text": "A hero image"
}'

The server reads the real content type from the stored bytes here. The type you declared in step 1 is a hint and is never trusted.

What the server checks, and when#

The content_type you declare in step 1 is a hint only. At registration the server reads the stored bytes and takes the real type from them. A file whose actual type is not an allowed image is rejected with 422 VALIDATION_FAILED and deleted from storage, so a rejected upload leaves nothing behind.
  • Accepted types, by their real bytes rather than their declared type: image/webp, image/png, image/jpeg, image/gif, image/avif.
  • size_bytes in step 1 lets an over-limit upload be refused before the bytes move. It is verified again afterwards.
  • max_size_bytes comes back in the reservation response. It is the hard ceiling for that upload.
  • Dimensions are read from the file. You do not send them.

If you never register#

An uploaded object with no media_assets row is not part of your library. It is swept and the reservation expires. Nothing is charged and nothing is left dangling, so an abandoned upload is safe to abandon.

The presigned URL expires on its own too. If your PUT is slow or fails, request a new reservation rather than retrying an expired signature.

Retrying a reservation#

The presigned URL is a bearer credential, so it is deliberately not replayed. A retry with the same Idempotency-Key returns the same upload_id with upload_url: null and upload_url_replayable: false. That is the honest answer: the retry has learnt that the reservation exists, and no live credential has been written to a table that gets backed up. Request a fresh reservation.

Deleting an asset#

Deleting a media asset does not rewrite your articles. If a published article embeds the image, or uses it as its featured_image_url, that reference becomes a broken image on your live blog.
  • GET /articles?fields=id,featured_image_url finds featured uses.
  • In body content the URL is plain text, so search your content for it.
  • The catalog row and the stored bytes both go. There is no undo.

Alt text#

Worth sending, and the only field you can edit after the fact. It is what screen readers announce and what search engines read, and there is no way to generate it for you. The bytes themselves are immutable: to replace an image, upload a new one and repoint whatever referenced the old one.