Skip to content
OpenPostDocs
OpenPostDocs

Media uploads

Upload bytes through OpenPost's storage-safe HTTP flow.

Media uploads usually use three requests. Your integration creates an upload session, sends bytes to its storage target, then completes the session. If OpenPost finds identical ready media in the workspace, it returns deduped: true and the returned media_id is ready to use without the upload and completion requests. The examples use Bash, curl, and jq.

1. Create a session

OPENPOST_BASE="https://app.openpo.st"
WORKSPACE_ID="ws_..."

SESSION_JSON="$(curl "$OPENPOST_BASE/api/v1/media/upload-session" \
  -X POST \
  -H "Authorization: Bearer $OPENPOST_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cover-2026-09-19" \
  --data "$(jq -n \
    --arg workspace_id "$WORKSPACE_ID" \
    --arg filename "cover.png" \
    --arg mime_type "image/png" \
    --arg alt_text "Product import screen with the completed file list" \
    '{workspace_id: $workspace_id, filename: $filename, mime_type: $mime_type, size: 48231, alt_text: $alt_text}')")"

MEDIA_ID="$(printf '%s' "$SESSION_JSON" | jq -r '.media_id')"
DEDUPED="$(printf '%s' "$SESSION_JSON" | jq -r '.deduped')"

The response includes media_id, deduped, complete_url, and an upload object with a method, URL, and headers. The following steps only run when DEDUPED is false.

2. Send the bytes

Use the exact method and headers from upload. An absolute target is external storage, so do not send the OpenPost bearer token to it. A relative target is an authenticated OpenPost endpoint.

if [ "$DEDUPED" = "false" ]; then
  UPLOAD_METHOD="$(printf '%s' "$SESSION_JSON" | jq -r '.upload.method')"
  UPLOAD_URL="$(printf '%s' "$SESSION_JSON" | jq -r '.upload.url')"
  UPLOAD_HEADERS=()
  while IFS= read -r header; do
    UPLOAD_HEADERS+=(-H "$header")
  done < <(printf '%s' "$SESSION_JSON" | jq -r '.upload.headers | to_entries[] | "\(.key): \(.value)"')

  case "$UPLOAD_URL" in
    http://* | https://*)
      UPLOAD_TARGET="$UPLOAD_URL"
      UPLOAD_EXTERNAL=true
      ;;
    /*)
      UPLOAD_TARGET="$OPENPOST_BASE$UPLOAD_URL"
      UPLOAD_EXTERNAL=false
      ;;
    *)
      printf 'Unsupported upload URL: %s\n' "$UPLOAD_URL" >&2
      exit 1
      ;;
  esac

  if [ "$UPLOAD_EXTERNAL" = "true" ]; then
    curl "$UPLOAD_TARGET" \
      -X "$UPLOAD_METHOD" \
      "${UPLOAD_HEADERS[@]}" \
      --data-binary @cover.png
  else
    curl "$UPLOAD_TARGET" \
      -X "$UPLOAD_METHOD" \
      -H "Authorization: Bearer $OPENPOST_TOKEN" \
      "${UPLOAD_HEADERS[@]}" \
      --data-binary @cover.png
  fi
fi

3. Complete the session

Complete the session with the returned media_id, your workspace ID, and your OpenPost bearer token. Completion starts the normal validation and media-processing flow.

if [ "$DEDUPED" = "false" ]; then
  RESULT_JSON="$(curl "$OPENPOST_BASE/api/v1/media/upload-session/$MEDIA_ID/complete" \
    -X POST \
    -H "Authorization: Bearer $OPENPOST_TOKEN" \
    -H "Content-Type: application/json" \
    --data "{\"workspace_id\":\"$WORKSPACE_ID\"}")"

  MEDIA_ID="$(printf '%s' "$RESULT_JSON" | jq -r '.id')"
fi

Use the completion response's id in Publication or Rendition media fields. When the create-session response is deduplicated, use its media_id instead and skip steps 2 and 3.

The create upload session reference contains the exact request and response schemas and links to completion. JavaScript and TypeScript integrations can use media.upload() to run all three steps.

Fit an image to a destination

After uploading an image, call POST /api/v1/media/{id}/prepare-image with a connected account_id, its explicit output_profile from capability resolution, and any destination settings used for that resolution. The account and image must belong to the same workspace, and the caller needs editor access. A scoped API token needs media:write.

{
  "account_id": "account_...",
  "output_profile": "instagram.feed",
  "alt_text": "The completed product import screen"
}

OpenPost resolves the connected account's limits, then pads and downscales the image without cropping. It may re-encode the copy to fit accepted formats and byte limits. JPEG padding is white; PNG and WebP preserve transparent padding. Animated images are rejected to avoid losing frames. Inputs and image allocations are bounded; an image that cannot fit returns an error.

The response contains status: "ready" and a separate media object, including its ID, known width and height, and parent_media_id. Preparation finishes before the response. Attach the returned ID explicitly. Existing posts and the original asset are unchanged. Repeating the same preparation reuses a matching ready copy when available. This operation prepares an image; validate the complete post separately before publishing.

On this page