JuzPost Storage API
Presigned direct-to-R2 media upload and download URLs.
Presigned direct-to-R2 media upload and download URLs.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/juzpost:juzpost-storage-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: JuzPost REST Storage API
version: 1.0.0
summary: Token-only API behind the `juzpost` command line client.
description: 'The machine-facing surface of JuzPost: connect-once device login, draft creation,
media upload to R2, and scheduling posts across connected social accounts.
**Base URL.** `https://www.juzpost.com/api/cli/v1`. Note the `cli` segment:
`/api/v1` is a different, older surface used by the dashboard and is not this API.
**Authentication.** Every operation requires `Authorization: Bearer `. There are no
unauthenticated operations.
## Get a token
1. Open **Settings, General** in the JuzPost
dashboard.
2. Find the **API keys** card and choose **Generate API key**.
3. Copy the key. It begins with `jp_live_` and is shown **once**, at creation. Store it in
your secret manager before leaving the page.
4. Check it works:
```bash
curl https://www.juzpost.com/api/cli/v1/me \
--header "Authorization: Bearer jp_live_YOUR_KEY"
```
A `200` comes back with your account, plan and capabilities. A `401` means the key is
wrong, revoked, or was pasted with a trailing newline.
Revoke a key from the same card. Nothing in this API can delete the credential it is
calling with, which is deliberate: an automated client should not be able to lock itself
out, and revocation should be a human action.
**One token, one account.** A token belongs to exactly one JuzPost account (a *workspace*).
Every operation acts on that one account, and no operation anywhere accepts an account id.
**Entitlements.** Every operation other than `GET /me` requires the
caller''s plan to include the `api` capability, and `POST /schedule`,
`POST /posts/{postId}/schedule` and `POST /storage/presign` additionally require `schedule`
(or `publish`, for immediate posting). A plan that lacks the capability produces `402`,
never `403`. Call `GET /me` first to check before attempting a write that is doomed.
**Errors.** Every non-2xx response uses the same envelope: a single `error` object carrying
a stable machine-readable `code`, a human `message`, and optional `details`. Never branch on
the message text; branch on `error.code` or the HTTP status.
**Unknown ids.** An id that is not yours is reported as `404`, never `403`, so no operation
can be used to discover whether someone else''s data exists.'
contact:
name: JuzPost Support
url: https://www.juzpost.com/support
license:
name: Proprietary
url: https://www.juzpost.com/terms
servers:
- url: https://www.juzpost.com/api/cli/v1
description: The only supported base URL.
security:
- bearerAuth: []
tags:
- name: Storage
description: Presigned direct-to-R2 media upload and download URLs.
paths:
/storage/presign:
post:
operationId: presignStorage
summary: Get a short-lived URL to upload media straight to storage
description: '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.'
tags:
- Storage
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PresignRequest'
responses:
'200':
description: A presigned URL valid for `expiresIn` seconds.
content:
application/json:
schema:
$ref: '#/components/schemas/PresignResponse'
'400':
description: 'Invalid body, neither `key` nor `relativeKey` supplied, a key containing characters
outside `A-Za-z0-9._-/`, or a missing `contentLength` on an upload.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
description: '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`.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
$ref: '#/components/responses/Forbidden'
'422':
description: 'The declared `contentLength` exceeds the size cap for that content type.
`error.details.code` is `file_too_large`.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
$ref: '#/components/responses/RateLimited'
components:
responses:
Unauthorized:
description: 'No bearer token was sent, or the token is unknown, revoked or malformed. Run the device
login again to obtain a new token.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: Too many requests in a short window. Back off and retry.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Forbidden:
description: '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.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
Error:
type: object
description: 'The envelope returned by every non-2xx response in this API. There is never a top-level
field other than `error`.
'
required:
- error
properties:
error:
type: object
description: The single error describing why the request did not succeed.
required:
- code
- message
properties:
code:
$ref: '#/components/schemas/ErrorCode'
message:
type: string
description: 'Human-readable explanation, safe to show to a user. Wording is not stable across
releases, so never parse or match on it.
'
details:
description: 'Optional structured context whose shape depends on the code, for example
`{ "requiredPlan": "pro" }` on a `payment_required`, `{ "code":
"invalid_cursor" }` on a bad cursor, or `{ "usedBytes": …, "quotaBytes": … }`
when a storage quota is full. Treat it as opaque unless a specific operation
documents its contents.
'
PresignResponse:
type: object
description: A signed URL and the key it points at.
required:
- signedUrl
- resolvedKey
- expiresIn
properties:
signedUrl:
type: string
format: uri
description: '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:
type: string
description: '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:
type: integer
description: Seconds the URL remains valid, counting from when it was issued.
PresignRequest:
type: object
description: '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.
'
required:
- contentType
properties:
relativeKey:
type: string
minLength: 1
description: '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:
type: string
minLength: 1
description: '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:
type: string
minLength: 1
description: '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:
type: integer
minimum: 1
description: '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:
type: string
description: '`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.
'
enum:
- upload
- download
default: upload
expiresInSeconds:
type: integer
minimum: 60
maximum: 86400
default: 300
description: '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.
'
ErrorCode:
type: string
description: 'Stable machine-readable error identifier. Branch on this rather than on the message.
Each code maps to exactly one HTTP status.
'
enum:
- bad_request
- unauthorized
- payment_required
- forbidden
- not_found
- conflict
- unprocessable
- pending
- rate_limited
- internal
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: 'An API key created in the dashboard under Settings, General, API keys, sent as
`Authorization: Bearer jp_live_...`. Required on every operation without exception.
Missing, malformed, revoked or unknown tokens all produce `401` with code
`unauthorized`.
'