Zernio
Zernio
Overview

Guides

ProfilesConnecting accountsMedia UploadsQueue SchedulingTimezones & SchedulingIdempotency & Safe RetriesPost LifecycleError HandlingRate LimitsPlatform Settings
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources
Guides

Media Uploads

Upload an image, video or document with a presigned URL and attach it to a post with mediaItems.


When you finish this page a file is uploaded to Zernio's storage and attached to a post. You need an API key and an accountId (quickstart). Uploads go straight to storage through a presigned URL, up to 5 GB per file, in 3 calls: POST /v1/media/presign, a PUT of the file, then POST /v1/posts with the returned publicUrl in mediaItems. A file you already host on a public HTTPS URL skips the first two calls; see Media URL requirements.

Step 1: Get an upload URL

Call POST /v1/media/presign with filename and contentType. Pass size in bytes to have the 5 GB limit checked before you upload.

import { readFile } from 'node:fs/promises';
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: presigned } = await zernio.media.getMediaPresignedUrl({
  body: {
    filename: 'photo.jpg',
    contentType: 'image/jpeg'
  }
});

const { uploadUrl, publicUrl } = presigned;

Response (200):

{
  "uploadUrl": "https://<bucket>.r2.cloudflarestorage.com/temp/1234567890_abc123_photo.jpg?X-Amz-Signature=...",
  "publicUrl": "https://media.zernio.com/temp/1234567890_abc123_photo.jpg",
  "key": "temp/1234567890_abc123_photo.jpg",
  "expiresIn": 3600
}

uploadUrl is valid for expiresIn seconds (1 hour). publicUrl is the address the post uses in Step 3.

Step 2: Upload the file

PUT the file to uploadUrl with the same Content-Type. This request goes to storage, not to the API, so it carries no Authorization header.

const fileBuffer = await readFile('photo.jpg');

await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'image/jpeg' },
  body: fileBuffer
});

Response (200), no body. Storage answers with an empty 200 and an ETag header, never JSON. The file is now at publicUrl.

Step 3: Create the post

Call POST /v1/posts with publicUrl in mediaItems. type is image, video, gif or document; it is inferred from the file extension when omitted, and a type that contradicts the extension is rejected with 400.

const { data: created } = await zernio.posts.createPost({
  body: {
    content: 'Golden hour at the pier.',
    mediaItems: [
      { url: publicUrl, type: 'image' }
    ],
    platforms: [
      { platform: 'linkedin', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
    ],
    scheduledFor: '2027-01-01T12:00:00',
    timezone: 'America/New_York'
  }
});

console.log(created.post._id);

Response (201):

{
  "message": "Post scheduled successfully",
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "status": "scheduled",
    "scheduledFor": "2027-01-01T17:00:00Z",
    "mediaItems": [
      { "url": "https://media.zernio.com/temp/1234567890_abc123_photo.jpg", "type": "image" }
    ],
    "platforms": [
      { "platform": "linkedin", "status": "pending" }
    ]
  }
}

The presign endpoint lists every parameter.

How it behaves

Uploads expire after 7 days

An upload sits in temporary storage for 7 days. When a post that references its publicUrl publishes, Zernio copies the file to permanent storage. Schedule posts that use an upload within 7 days of uploading.

Media URL requirements

Every mediaItems[].url must be publicly reachable over HTTPS and return the file itself. Google Drive, Dropbox, OneDrive and iCloud links do not work: they return an HTML page, not the file, so the platform's servers cannot fetch from them. Use a direct media URL or upload through the presign endpoint. Test a URL in an incognito window: a webpage instead of the raw file means the post will fail.

Formats and sizes

contentType must be one of the MIME types the presign endpoint accepts. A generic type such as application/octet-stream is rejected with 400.

TypeFormats
ImagesJPEG, PNG, GIF, WebP
VideosMP4, MPEG, MOV, AVI, WebM, M4V
DocumentsPDF (LinkedIn only)
AudioMP3, M4A, AAC, OGG, WAV, WebM

Audio has no mediaItems[].type, so an audio file cannot be attached to a post. Send its publicUrl as attachmentUrl with attachmentType: "audio" on send inbox message instead.

Presign caps every type at 5 GB; no type has a smaller cap. Platforms are stricter: a LinkedIn document must be a PDF of 100 MB or less, and each platform's limits on count, size, duration and aspect ratio are under "Media requirements" on its platform page.

Zernio compresses files above a platform's limit

Images and videos above a platform's limit are compressed before upload; videos over 200 MB may not be. Bluesky images are recompressed to stay under its 1 MB blob limit. A custom thumbnail in mediaItems[].thumbnail (JPG or PNG, 10 MB max) applies to Facebook video posts, Facebook Reels, YouTube videos and LinkedIn video posts.

customMedia replaces mediaItems for one platform

mediaItems attaches to every platform in the request. To send a different file to one of them, set customMedia on that entry:

{
  "content": "Same text, different media per platform",
  "mediaItems": [{ "url": "https://media.zernio.com/temp/1234567890_abc123_photo.jpg", "type": "image" }],
  "platforms": [
    { "platform": "linkedin", "accountId": "66b2e19d8c3f5a7e9d0b1c2d" },
    {
      "platform": "instagram",
      "accountId": "66b2e19d8c3f5a7e9d0b1c2e",
      "customMedia": [{ "url": "https://media.zernio.com/temp/1234567890_abc123_square.jpg", "type": "image" }]
    }
  ]
}

If it fails

A 400 from POST /v1/media/presign means filename is missing, contentType is not an accepted MIME type, or size is above 5 GB:

{
  "error": "Invalid contentType",
  "type": "invalid_request_error",
  "code": "invalid_field_value",
  "param": "contentType"
}

Send the file's real MIME type (image/jpeg, video/mp4, application/pdf). A PUT to an uploadUrl older than 1 hour is refused by storage; request a new one.

Related

  • Presign endpoint: every parameter and accepted MIME type.
  • Create post: mediaItems, customMedia, altText and thumbnail.
  • Platforms: per-platform media requirements.
  • Multi-tenant publishing: uploading once for many customers.
Was this page helpful?

Connecting accounts

Connect an account to a profile with OAuth, a hosted or headless selection step, or credentials for Bluesky, Telegram and Shopify.

Queue Scheduling

Give a profile recurring posting slots and create posts with queuedFromProfile so each one lands on the next free slot.

On this page

Step 1: Get an upload URLStep 2: Upload the fileStep 3: Create the postHow it behavesUploads expire after 7 daysMedia URL requirementsFormats and sizesZernio compresses files above a platform's limitcustomMedia replaces mediaItems for one platformIf it failsRelated