---
title: "Get a short-lived URL to upload media straight to storage"
method: POST
path: "/storage/presign"
tags: ["Storage"]
---

# Get a short-lived URL to upload media straight to storage

`POST /storage/presign`

## Request

- Base URL: `https://www.juzpost.com/api/cli/v1`
- URL: `https://www.juzpost.com/api/cli/v1/storage/presign`
- Auth: HTTP bearer

Issues a presigned URL so file bytes go directly to object storage instead of through
this API. For an upload, PUT the raw bytes to `signedUrl` with the same `Content-Type`
and exactly the `contentLength` you declared. Both are bound into the signature and a
mismatch is rejected by storage. Then pass the returned `resolvedKey` as a `mediaUrls`
entry or `coverR2Key` when creating a post. Keys are forced under your own storage
prefix, so you cannot presign into another tenant's space. Uploads are capped at 500 MB
for video, 15 MB for images and 10 MB otherwise, rate-limited to 20 presigns per minute
per account, and counted against your storage quota.

## Request body

- PresignRequest: What you want to upload or download. Supply exactly one of `relativeKey` (preferred) or `key`; both resolve to a path under your own storage prefix.
  - `relativeKey` string: Path for the object relative to your storage prefix, such as `media/clip_03.mp4`. Only letters, digits, `.`, `_`, `-` and `/` are allowed, and `..` segments are rejected. The server prepends your storage prefix and returns the result as `resolvedKey`.
  - `key` string: A full storage key, used instead of `relativeKey`, normally only for `operation: download` of a key you already hold. It must already start with your own storage prefix or the request is refused with `403`.
  - `contentType` string, required: MIME type of the object, such as `video/mp4` or `image/jpeg`. It is bound into the signature, so the PUT must send exactly this same `Content-Type`. It also selects the size cap: 500 MB for `video/*`, 15 MB for `image/*`, 10 MB for anything else.
  - `contentLength` integer: Exact byte length of the object you are about to upload. Required for uploads and ignored for downloads. It is bound into the signature, so a PUT of a different size is rejected by storage, and it is counted against your storage quota as soon as the URL is issued.
  - `operation` 'upload' | 'download': `upload` presigns a PUT for new bytes; `download` presigns a GET for an object that already exists. Downloads skip the quota and rate-limit checks.
  - `expiresInSeconds` integer: How long the returned URL stays valid, between 60 seconds and one day. Keep it short: whoever holds the URL can use it until it expires. Allow generous client-side timeouts for large uploads within this window.

## Response `200`

A presigned URL valid for `expiresIn` seconds.

- PresignResponse: A signed URL and the key it points at.
  - `signedUrl` string, uri, required: Send the raw bytes here with a plain PUT (or GET, for a download) and no `Authorization` header. The signature in the URL is the credential. Never log or share this URL.
  - `resolvedKey` string, required: The storage key the URL writes to. This is the value to pass as a `mediaUrls` entry or `coverR2Key` when creating or updating a post.
  - `expiresIn` integer, required: Seconds the URL remains valid, counting from when it was issued.

## Other responses

- `400`: Invalid body, neither `key` nor `relativeKey` supplied, a key containing characters outside `A-Za-z0-9._-/`, or a missing `contentLength` on an upload.
- `401`: No bearer token was sent, or the token is unknown, revoked or malformed. Run the device login again to obtain a new token.
- `402`: Either the plan lacks the `api`/`schedule` capability, or your storage quota is already full. In the quota case `error.details` carries `usedBytes` and `quotaBytes`.
- `403`: The token is valid but the request reaches outside what it is allowed to touch, such as a storage key outside the caller's own storage prefix.
- `422`: The declared `contentLength` exceeds the size cap for that content type. `error.details.code` is `file_too_large`.
- `429`: Too many requests in a short window. Back off and retry.

---

[API](https://skmtc.dev/juzpost/apis/juzpost-rest-api.md) · [All operations](https://skmtc.dev/juzpost/apis/juzpost-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/juzpost/apis/juzpost-rest-api/revisions/19296da24f95?raw)
