Zernio
Zernio
OverviewBuild a PlatformPublishingAnalyticsInbox and DMs
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources
Build a Platform

Publishing

Publish for every customer with an idempotency key, one media upload per asset, each customer's own queue and outcomes from webhooks.


When you finish this page your app creates posts for every customer without duplicates, uploads each file once, spreads publishing across each customer's queue and learns the outcome from webhooks. You need a customer's profileId and their account ids from the core model. A duplicate here goes out on your customer's account, which is why each step is retry-safe.

Step 1: Upload media once

Call POST /v1/media/presign, PUT the file to uploadUrl, and put publicUrl in the post's mediaItems. One upload serves every platform entry in the post and can be reused across posts. Uploads sit in temporary storage for 7 days and are copied to permanent storage when you create the post that uses them. Create the post within 7 days of the upload, or upload when you create the post rather than when the customer picks the file. The media uploads guide has the full flow.

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

const zernio = new Zernio();
const profileId = '66a1f0c2a4b9d3e8f1a2b3c4';

const { data: presigned } = await zernio.media.getMediaPresignedUrl({
  body: { filename: 'launch.mp4', contentType: 'video/mp4' },
});

const fileBuffer = await readFile('launch.mp4');

await fetch(presigned.uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'video/mp4' },
  body: fileBuffer,
});

Response (200) from the presign call:

{
  "uploadUrl": "https://...",
  "publicUrl": "https://media.zernio.com/temp/1234567890_abc123_launch.mp4",
  "expiresIn": 3600
}

Response (200) from the PUT, with no body: storage answers with an empty 200 and an ETag header. From then on the file is served at publicUrl.

Step 2: Create the post with an idempotency key

Call POST /v1/posts with an Idempotency-Key header, the customer's account ids and queuedFromProfile set to their profileId. Generate one UUID per logical post and send the same UUID on every retry of that call; a replay within 24 hours returns 200 with the original post in post. The Python SDK sends it as x_request_id (the x-request-id header), which returns the original post only when the retry's content is identical. Treat a replay and a 409 (same content to the same account within 24 hours) as success. The idempotency guide covers every layer.

queuedFromProfile places the post on the next free slot of that customer's queue, so each profile drips its own posts with no scheduler of your own. For a post that must go out at an exact moment, send scheduledFor and timezone instead. For a customer's CSV import, use POST /v1/posts/bulk-upload with dryRun=true first.

const { data: listed } = await zernio.accounts.listAccounts({
  query: { profileId },
});

const { data: queued } = await zernio.posts.createPost({
  headers: { 'Idempotency-Key': randomUUID() },
  body: {
    content: 'Big news coming Friday.',
    mediaItems: [{ type: 'video', url: presigned.publicUrl }],
    platforms: listed.accounts.map((a) => ({
      platform: a.platform,
      accountId: a._id,
    })),
    queuedFromProfile: profileId,
  },
});

console.log(queued.post.status, queued.post.scheduledFor);

Response (201):

{
  "message": "Post scheduled successfully",
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "status": "scheduled",
    "scheduledFor": "2027-01-04T14:00:00Z",
    "queuedFromProfile": "66a1f0c2a4b9d3e8f1a2b3c4",
    "platforms": [
      { "platform": "instagram", "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "status": "pending" }
    ]
  }
}

POST /v1/posts accepts any accountId your team owns, whichever profile it sits in. Pass a customer only the account ids stored against them.

A scheduler that fires every customer's posts with publishNow: true on the hour creates a burst that spends your request budget at once and runs into per-account velocity limits. Queue slots spread the same posts across the hour.

Step 3: Read the outcome from webhooks

The 201 means accepted. What happened at publish time arrives on the post webhooks: post.published when every platform succeeded, post.partial or post.failed otherwise, and post.platform.published / post.platform.failed per platform as each one finishes. Route each event to the customer with profiles[0].externalId (a post that spans several customers' profiles lists each one), then update your UI from post.status (post lifecycle). Do not poll GET /v1/posts to discover status changes; polling is for reconciliation.

A post.published delivery for the post above:

{
  "id": "evt_5f8e2a1c",
  "event": "post.published",
  "post": {
    "id": "65f1c0a9e2b5af0012ab34cd",
    "status": "published",
    "scheduledFor": "2027-01-04T14:00:00Z",
    "publishedAt": "2027-01-04T14:00:03Z",
    "platforms": [
      {
        "platform": "instagram",
        "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
        "status": "published",
        "publishedUrl": "https://www.instagram.com/p/..."
      }
    ]
  },
  "timestamp": "2027-01-04T14:00:03Z"
}

If a published post is later deleted on the platform, post.platform.deleted fires (poll-driven, roughly hourly), so your UI can stop showing it as live.

If it fails

A 409 from Step 2 means the same content is already scheduled or was posted to that account in the last 24 hours:

{
  "error": "This exact content is already scheduled, publishing, or was posted to this account within the last 24 hours.",
  "details": {
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "platform": "instagram",
    "existingPostId": "65f1c0a9e2b5af0012ab34cd"
  }
}

Treat it as done and show the customer existingPostId. To repost the same content on purpose, change the caption, the media or the account.

Related

  • Build a platform: the profile-per-customer model.
  • Idempotency: Idempotency-Key, x-request-id and content-hash dedup in depth.
  • Queue scheduling: slots, several queues per profile, previewing the next slot.
  • Media uploads: presigned URLs, size limits, per-platform rules.
  • Post lifecycle: every status and transition.
Was this page helpful?

Build a Platform

Give each of your customers a profile, connect their accounts into it, post on their behalf and route webhooks back to the right customer.

Analytics

Serve each customer their own analytics dashboard from your database, filled by a sync worker that reads GET /v1/analytics with profileId.

On this page

Step 1: Upload media onceStep 2: Create the post with an idempotency keyStep 3: Read the outcome from webhooksIf it failsRelated