Doc

Upload media

Create a presigned upload URL for media used by the Social Service publish API

This guide is for developers who need to publish local media files. The upload endpoint creates a temporary presigned PUT URL. It does not upload file bytes by itself.

After the PUT upload succeeds, use the returned temporaryUrl.url or publishAsset.source as the media source in /api/social/v1/post.

Endpoint

POST /api/social/v1/media/upload

Production URL:

https://sogrowly.com/api/social/v1/media/upload

Authentication

Requests require a Bearer Social API key. The API key must include the social:publish permission:

Authorization: Bearer sma_live_xxx
Content-Type: application/json

Request Body

{
  "mimeType": "image/png",
  "fileName": "post-image.png"
}

Fields:

mimeType  Required. Supported values: image/png, image/jpeg, image/webp, video/mp4, application/pdf.
fileName  Optional. Used to build a safe object key. If omitted, the service uses a generated name.

The uploaded object is stored under the tmp_file_upload/ prefix. The service generates the final object key and ensures the file extension matches the MIME type.

Successful Response

{
  "status": "success",
  "statusCode": 200,
  "data": {
    "objectKey": "tmp_file_upload/uuid-post-image.png",
    "mimeType": "image/png",
    "assetType": "image",
    "upload": {
      "method": "PUT",
      "url": "https://example-presigned-upload-url",
      "headers": {
        "Content-Type": "image/png"
      },
      "expiresInSeconds": 300,
      "expiresAt": "2026-06-10T10:05:00.000Z"
    },
    "temporaryUrl": {
      "url": "https://files.example.com/tmp_file_upload/uuid-post-image.png",
      "expiresInSeconds": 86400,
      "expiresAt": "2026-06-11T10:00:00.000Z",
      "availableAfterUpload": true
    },
    "publishAsset": {
      "type": "image",
      "source": "https://files.example.com/tmp_file_upload/uuid-post-image.png",
      "sourceType": "url",
      "mimeType": "image/png"
    }
  },
  "message": "Media upload URL created. Upload the file bytes with HTTP PUT before using the temporary URL.",
  "meta": {
    "version": "v1"
  }
}

Important: temporaryUrl.url is not usable until the file bytes are uploaded successfully to upload.url.

Upload the File Bytes

Use the returned upload.method, upload.url, and upload.headers to upload the local file bytes:

const uploadResponse = await fetch(result.data.upload.url, {
  method: result.data.upload.method,
  headers: result.data.upload.headers,
  body: fileBytes,
});

if (!uploadResponse.ok) {
  throw new Error(`Upload failed: ${uploadResponse.status}`);
}

Browser example with a File object:

const uploadResponse = await fetch(result.data.upload.url, {
  method: 'PUT',
  headers: result.data.upload.headers,
  body: file,
});

cURL example:

curl -X PUT "https://example-presigned-upload-url" \
  -H "Content-Type: image/png" \
  --data-binary "@post-image.png"

Publish With Uploaded Media

After the upload succeeds, pass the returned publishAsset to /api/social/v1/post:

{
  "workspaceId": "sws_123",
  "draft": {
    "text": "New product image.",
    "assets": [
      {
        "type": "image",
        "source": "https://files.example.com/tmp_file_upload/uuid-post-image.png",
        "sourceType": "url",
        "mimeType": "image/png"
      }
    ]
  },
  "targets": [
    {
      "channelId": "sch_123"
    }
  ]
}

The publish target uses channelId. Query publishable channels from /api/social/v1/account before publishing.

Common Errors

Unsupported MIME type:

{
  "status": "failed",
  "statusCode": 400,
  "data": {},
  "message": "Request body is invalid.",
  "error": {
    "code": "invalid_payload",
    "message": "Request body is invalid.",
    "details": {
      "issues": [
        {
          "code": "invalid_value",
          "path": "mimeType",
          "message": "Invalid option: expected one of ..."
        }
      ]
    }
  },
  "meta": {
    "version": "v1"
  }
}

API key missing the publish permission:

{
  "status": "failed",
  "statusCode": 403,
  "data": {},
  "message": "API key requires \"social:publish\" permission.",
  "error": {
    "code": "workspace_permission_denied",
    "message": "API key requires \"social:publish\" permission."
  },
  "meta": {
    "version": "v1"
  }
}

Status Codes and Rate Limits

200 -> Upload URL created
400 -> Invalid request body or unsupported MIME type
401 -> API key is missing, malformed, invalid, or expired
403 -> API key permission is insufficient
429 -> Rate limited by userId
500 -> Configuration error or unknown error

Current rate limit:

Each resolved userId can make up to 15 requests per minute.
The account, media upload, and post endpoints share this rate limit.