<!-- Generated from the canonical OpenPost public page. Do not edit this build artifact. -->

Title: Media uploads
Description: Upload bytes through OpenPost's storage-safe HTTP flow.
Canonical: https://docs.openpo.st/automate/api/media
Source: [https://docs.openpo.st/automate/api/media](https://docs.openpo.st/automate/api/media)

# Media uploads

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

```bash
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.

```bash
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.

```bash
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](https://docs.openpo.st/api-reference/media/create-media-upload-session) contains the exact request and response schemas and links to completion. JavaScript and TypeScript integrations can use [`media.upload()`](https://docs.openpo.st/automate/sdk/media) to run all three steps.
