JuzPost Storage API

Presigned direct-to-R2 media upload and download URLs.

GitHub OpenAPI

Operations 1

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

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • 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.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/juzpost:juzpost-storage-api"
All apis
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.

Get an API 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 Specification

juzpost-storage-api-openapi.yml Raw ↑
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`.

        '