reference
Media
The media library. Two step upload, then registration.
/mediaList media assets#
- Scope
- media:read
- Rate limit
- 600/min (read)
- Publishable key
- refused
- Dashboard permission
- media.manage
The media library, newest first.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| cursor | query | string | The opaque cursor from |
| limit | query | integer | Page size. Values above the maximum are clamped rather than rejected, so a client asking for a thousand rows gets a hundred and a default 20 |
| bucket | query | MediaBucket | one of blog-images, author-avatars |
| fields | query | string | Comma separated field allow list. Default projection: |
Responses
- 200a page of MediaAsset
A page of media assets.
- 400
INVALID_REQUEST. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header. - 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_KEY_EXPIRED. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. - 403
INSUFFICIENT_SCOPE. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it.This is the only 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists.
- 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-After.Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.
Class Endpoints Limit read every GET 600 per minute write POST, PATCH and DELETE on content, taxonomy, keys and webhooks 120 per minute upload POST /media/upload-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin the body, so quote it if you contact support.Safe to retry, and safer still with the same
Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded. - 503
MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
curl https://api.writavo.com/v1/media \
-H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.
/mediaRegister an uploaded file#
- Scope
- media:write
- Rate limit
- 120/min (write)
- Publishable key
- refused
- Another Site's id
- 404
- Dashboard permission
- media.manage
Step three of the upload. Call this after your PUT to the presigned URL succeeds.
You send the upload_id you were given, not a path. The server already knows where the file went, which is what stops a client naming an arbitrary storage location.
The server inspects the stored bytes here: it reads the real content type from the file itself and checks the size and image dimensions. 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. The declared type you sent to /media/upload-url is treated as a hint only and never trusted.
If you never call this, the uploaded object is swept and the reservation expires. A file with no media_assets row is not part of your library.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| Idempotency-Key* | header | string | A unique key you generate per logical operation, 1 to 255 printable ASCII characters. A UUID is the obvious choice. Retry with the same key and the same body and you get the original response replayed rather than a second object. This is what makes a network timeout safe: you never know whether the first request landed, so you retry with the same key and find out. Same key with a different body is Keys are scoped to the Site and the endpoint, and are retained for 24 hours. After that the same key is a new operation. |
Request body
| Field | Type | Notes |
|---|---|---|
| upload_id* | uuid | From |
| alt_text | string | null | Accessibility text. Worth sending. It is what screen readers announce and what search engines read, and there is no way to generate it for you. |
Responses
- 201MediaAsset
The asset is registered and in your library.
LocationThe canonical URL of the new asset.
- 400
INVALID_REQUEST. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header. - 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_KEY_EXPIRED. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. - 403
INSUFFICIENT_SCOPE. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it.This is the only 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists.
- 404
NOT_FOUND. Either no such object exists, or it exists and belongs to a different Site.These two cases are deliberately indistinguishable, and neither ever returns 403. A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site.
- 409
SLUG_CONFLICT,IDEMPOTENCY_KEY_CONFLICT,IDEMPOTENCY_KEY_IN_FLIGHTorCONFLICT. The request is valid but collides with the current state: a slug is taken, an idempotency key was reused with a different body or is still in flight, or the object moved while you were working on it. Readcodeto tell which, then re-read and retry. - 422
VALIDATION_FAILED. The request parsed but the values are not acceptable.error.fieldsmaps each offending field to a message you can put next to the input.Common causes: publishing without a title, slug or content; scheduling in the past; and sending
statuson a create or update, which is how the API refuses to let a client push content into the AI pipeline. - 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-After.Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.
Class Endpoints Limit read every GET 600 per minute write POST, PATCH and DELETE on content, taxonomy, keys and webhooks 120 per minute upload POST /media/upload-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin the body, so quote it if you contact support.Safe to retry, and safer still with the same
Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded. - 503
MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
curl -X POST https://api.writavo.com/v1/media \
-H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"upload_id": "3f1b0c7a-0000-4000-8000-000000000001"
}'Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.
/media/upload-urlRequest an upload URL#
- Scope
- media:write
- Rate limit
- 60/min (upload)
- Publishable key
- refused
- Dashboard permission
- media.manage
Step one of the upload. Returns a short lived presigned URL and an upload_id.
The upload is a three step handshake:
POST /media/upload-urlwith the filename and declared content type.PUTthe raw bytes toupload_url. Send no authorization header; the signature in the URL is the credential.POST /mediawith theupload_idto register the asset.
The storage location is chosen by the server and namespaced to your Site. You cannot influence it, and no other Site's key can produce a URL that writes into your namespace.
The URL expires. If your PUT is slow or fails, request a new one; do not retry an expired signature.
upload_url is a bearer credential, so it is not replayed: a retry with the same Idempotency-Key returns the same upload_id with upload_url: null and upload_url_replayable: false. Request a fresh reservation instead.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| Idempotency-Key* | header | string | A unique key you generate per logical operation, 1 to 255 printable ASCII characters. A UUID is the obvious choice. Retry with the same key and the same body and you get the original response replayed rather than a second object. This is what makes a network timeout safe: you never know whether the first request landed, so you retry with the same key and find out. Same key with a different body is Keys are scoped to the Site and the endpoint, and are retained for 24 hours. After that the same key is a new operation. |
Request body
| Field | Type | Notes |
|---|---|---|
| file_name* | string | The original filename. It is sanitised before use and never becomes a path on its own, so traversal sequences are stripped rather than rejected. |
| content_type* | string | The MIME type you believe you are uploading. A hint only. The real type is read from the bytes at registration and it is that check, not this field, that decides whether the file is accepted. one of image/webp, image/png, image/jpeg, image/gif, image/avif |
| size_bytes | integer | Declared size, so an over limit upload can be refused before the bytes move. Also verified after the fact. |
| bucket | MediaBucket | Which library the asset belongs to. |
Responses
- 201
The upload was reserved.
Field Type Notes upload_id* uuid upload_url* uri Presigned. Do not log it; it is a bearer credential until it expires.
method* "PUT" headers object Headers you must send with the PUT, if any.
expires_at* date-time max_size_bytes integer The hard byte ceiling for this upload.
- 400
INVALID_REQUEST. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header. - 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_KEY_EXPIRED. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. - 403
INSUFFICIENT_SCOPE. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it.This is the only 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists.
- 409
SLUG_CONFLICT,IDEMPOTENCY_KEY_CONFLICT,IDEMPOTENCY_KEY_IN_FLIGHTorCONFLICT. The request is valid but collides with the current state: a slug is taken, an idempotency key was reused with a different body or is still in flight, or the object moved while you were working on it. Readcodeto tell which, then re-read and retry. - 422
VALIDATION_FAILED. The request parsed but the values are not acceptable.error.fieldsmaps each offending field to a message you can put next to the input.Common causes: publishing without a title, slug or content; scheduling in the past; and sending
statuson a create or update, which is how the API refuses to let a client push content into the AI pipeline. - 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-After.Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.
Class Endpoints Limit read every GET 600 per minute write POST, PATCH and DELETE on content, taxonomy, keys and webhooks 120 per minute upload POST /media/upload-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin the body, so quote it if you contact support.Safe to retry, and safer still with the same
Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded. - 503
MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
curl -X POST https://api.writavo.com/v1/media/upload-url \
-H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"file_name": "hero.webp",
"content_type": "image/webp"
}'Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.
/media/{id}Read one media asset#
- Scope
- media:read
- Rate limit
- 600/min (read)
- Publishable key
- refused
- Another Site's id
- 404
- Dashboard permission
- media.manage
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid |
Responses
- 200MediaAsset
The media asset.
ETagThe concurrency token.
- 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_KEY_EXPIRED. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. - 403
INSUFFICIENT_SCOPE. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it.This is the only 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists.
- 404
NOT_FOUND. Either no such object exists, or it exists and belongs to a different Site.These two cases are deliberately indistinguishable, and neither ever returns 403. A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site.
- 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-After.Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.
Class Endpoints Limit read every GET 600 per minute write POST, PATCH and DELETE on content, taxonomy, keys and webhooks 120 per minute upload POST /media/upload-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin the body, so quote it if you contact support.Safe to retry, and safer still with the same
Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded. - 503
MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
curl https://api.writavo.com/v1/media/3f1b0c7a-0000-4000-8000-000000000001 \
-H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.
/media/{id}Update a media asset#
- Scope
- media:write
- Rate limit
- 120/min (write)
- Publishable key
- refused
- Another Site's id
- 404
- Dashboard permission
- media.manage
Only alt_text is editable. The bytes are immutable: to replace an image, upload a new one and repoint whatever referenced the old one.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid | |
| If-Match | header | string | The Optional in v1 for compatibility. Omitting it means last write wins. Send it. |
Request body
| Field | Type | Notes |
|---|---|---|
| alt_text | string | null |
Responses
- 200MediaAsset
The updated media asset.
ETagThe new concurrency token.
- 400
INVALID_REQUEST. The request could not be parsed, or a parameter is not usable: bad JSON, an unknown query parameter value, or a missing required header. - 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_KEY_EXPIRED. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. - 403
INSUFFICIENT_SCOPE. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it.This is the only 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists.
- 404
NOT_FOUND. Either no such object exists, or it exists and belongs to a different Site.These two cases are deliberately indistinguishable, and neither ever returns 403. A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site.
- 412
PRECONDITION_FAILED. YourIf-Matchdid not match the current version, meaning somebody edited the object since you read it. Nothing was written. Re-read, merge, and retry with the newETag. - 422
VALIDATION_FAILED. The request parsed but the values are not acceptable.error.fieldsmaps each offending field to a message you can put next to the input.Common causes: publishing without a title, slug or content; scheduling in the past; and sending
statuson a create or update, which is how the API refuses to let a client push content into the AI pipeline. - 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-After.Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.
Class Endpoints Limit read every GET 600 per minute write POST, PATCH and DELETE on content, taxonomy, keys and webhooks 120 per minute upload POST /media/upload-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin the body, so quote it if you contact support.Safe to retry, and safer still with the same
Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded. - 503
MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
curl -X PATCH https://api.writavo.com/v1/media/3f1b0c7a-0000-4000-8000-000000000001 \
-H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000" \
-H 'If-Match: W/"1767225600000"' \
-H "Content-Type: application/json" \
-d '{}'Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.
/media/{id}Delete a media asset#
- Scope
- media:write
- Rate limit
- 120/min (write)
- Publishable key
- refused
- Another Site's id
- 404
- Dashboard permission
- media.manage
Removes the catalog row and the stored bytes.
Articles are not rewritten. If a published article embeds this image, or uses it as its featured_image_url, that reference becomes a broken image on your live blog. Check before you delete: GET /articles?fields=id,featured_image_url finds featured uses, and in body content the URL is plain text you can search for.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | uuid |
Responses
- 204
Deleted. No body.
- 401
INVALID_API_KEY,API_KEY_REVOKEDorAPI_KEY_EXPIRED. There is no key, or it is not usable. This response never distinguishes "no such key" from "wrong key", so a caller cannot probe for valid keys. - 403
INSUFFICIENT_SCOPE. The key is valid but does not carry the scope this operation requires, or its creator's permissions no longer cover it.This is the only 403 in the API. In particular it is never returned for an object belonging to another Site: that case is always 404, so this response never reveals that something exists.
- 404
NOT_FOUND. Either no such object exists, or it exists and belongs to a different Site.These two cases are deliberately indistinguishable, and neither ever returns 403. A 403 would confirm the object exists, which would let anyone with a valid key enumerate other customers' content by id. If you are certain the id is right, check you are using the key for the correct Site.
- 429
RATE_LIMIT_EXCEEDED. Back off and honourRetry-After.Limits are per key, per minute, by endpoint class. Writes are limited harder than reads because they cost more to serve, and pipeline runs hardest of all because they cost real money.
Class Endpoints Limit read every GET 600 per minute write POST, PATCH and DELETE on content, taxonomy, keys and webhooks 120 per minute upload POST /media/upload-url60 per minute pipeline POST /pipeline/runs10 per minute Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, so you can slow down before you are refused rather than after.RateLimit-LimitRequests permitted in the current window for this endpoint class.RateLimit-RemainingRequests left in the current window.RateLimit-ResetUnix seconds at which the window resets.Retry-AfterSeconds to wait before retrying.
- 500
INTERNAL_ERROR. Something failed on our side. The message is deliberately generic; the detail is in our logs against therequest_idin the body, so quote it if you contact support.Safe to retry, and safer still with the same
Idempotency-Key, which guarantees you do not create a second object if the first request actually succeeded. - 503
MAINTENANCE. Writes are paused, either platform wide or for your Site. Reads usually keep working, and your published blog is served from cache and stays up. Retry after the window given inRetry-After.Retry-AfterSeconds to wait before retrying.
Request shape
curl -X DELETE https://api.writavo.com/v1/media/3f1b0c7a-0000-4000-8000-000000000001 \
-H "Authorization: Bearer wv_sk_EXAMPLE0000000000000000000000000000"Generated from the specification, so it shows the path, the required headers and the body shape. The guides carry the end to end examples.
Schemas
The shapes this resource sends and returns. Unknown fields may be added within /v1, so tolerate them.
MediaAsset#
A file in the media library. The storage location is not part of this contract: you get a url you can use, and the path behind it is ours to change.
| Field | Type | Notes |
|---|---|---|
| id* | uuidread only | |
| bucket* | MediaBucket | Which library the asset belongs to. |
| url* | uriread only | The public URL. Put this in |
| file_name | string | nullread only | |
| mime_type | string | nullread only | The type read from the bytes at registration, not the type you declared. |
| size_bytes | integer | nullread only | |
| width | integer | nullread only | |
| height | integer | nullread only | |
| alt_text | string | null | |
| created_at | date-timeread only |
MediaBucket#
Which library the asset belongs to. blog-images for article imagery, author-avatars for byline portraits.
blog-imagesauthor-avatars