Tenant Publishing
Publish and schedule on behalf of every customer - retry-safe creation, media done once, per-tenant queues instead of cron storms, and outcomes from webhooks.
Publishing is the write path every tenant uses daily — and the surface where integration mistakes hurt the most, because the failure mode is visible on your customer's social accounts. The recipe: create with an idempotency key, upload media once, schedule through per-tenant queues, and learn outcomes from webhooks.
Post on a tenant's behalf
You already have everything you need from the core model: the customer's profileId and their account IDs. List their accounts, build the platforms array, create the post:
const { data: accounts } = await zernio.accounts.listAccounts({
query: { profileId: customer.zernioProfileId },
});
await zernio.posts.createPost({
headers: { 'x-request-id': randomUUID() }, // retry-safe, see below
body: {
content: 'Big news coming Friday 👀',
platforms: accounts.accounts.map((a) => ({
platform: a.platform,
accountId: a._id,
})),
queuedFromProfile: customer.zernioProfileId, // next free slot in their queue
},
});accounts = client.accounts.list_accounts(profile_id=customer.zernio_profile_id)
client.posts.create_post(
x_request_id=str(uuid.uuid4()), # retry-safe, see below
content="Big news coming Friday 👀",
platforms=[
{"platform": a["platform"], "accountId": a["_id"]}
for a in accounts["accounts"]
],
queued_from_profile=customer.zernio_profile_id, # next free slot in their queue
)curl -X POST "https://zernio.com/api/v1/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "x-request-id: 4b4986f4-77b5-4c22-a3a7-2c56f7a657e1" \
-d '{
"content": "Big news coming Friday 👀",
"platforms": [{ "platform": "instagram", "accountId": "ACCOUNT_ID" }],
"queuedFromProfile": "PROFILE_ID"
}'Tenant isolation on this endpoint is your job. Posts validate accountId against your whole team, not against a profile — the API will happily publish to any account you own. Only ever pass account IDs from that customer's profile (your accountId → customer map from the core guide is the guard).
Make creation retry-safe
Your tenants' posts flow through queues, workflow tools, and mobile clients that all retry on timeouts — and a duplicate post goes out on your customer's brand account, not yours. Two layers protect you; use both deliberately:
| Layer | Keyed on | Window | On replay |
|---|---|---|---|
x-request-id header | The UUID you send | ~5 minutes | 200 with the original post in existingPost |
| Content-hash dedup | (platform, accountId, content + media) | 24 hours | 409 with existingPostId |
The working recipe: generate a fresh UUID per logical post, reuse the same UUID when retrying that exact call, and treat both replay responses as success — 200 + existingPost means your retry was absorbed; 409 means the content already went out, and existingPostId tells you which post it was. Full details in Idempotency & safe retries.
Upload media once, reference it everywhere
Media goes through presigned uploads: POST /v1/media/presign → PUT the file to uploadUrl → use publicUrl in the post's mediaItems. Two multi-tenant rules keep this efficient:
- One upload per asset, not per platform. A tenant posting one video to five platforms is one upload — the same
publicUrlgoes in all five platform entries, and it can be reused across posts too. - Upload near publish time. Uploads sit in temporary storage for 7 days; media is only copied to permanent storage when a post using it publishes. If your product schedules weeks ahead, store the tenant's file on your side and run the presign + upload when you create the post — not when the tenant picks the file.
Schedule through queues, not cron storms
The tempting design — your scheduler fires every tenant's 9:00 posts with publishNow: true at 9:00 sharp — creates a thundering herd: hundreds of simultaneous publishes that burn your request budget in bursts and run into per-account platform velocity limits.
Queues fix this structurally, and they're already per-tenant: each profile has its own queue with its own slots. Pass queuedFromProfile: <their profileId> and each post lands on that customer's next free slot — drip-feeding per tenant with no scheduler of your own:
| Instead of | Do this |
|---|---|
Computing scheduledFor for every post yourself | queuedFromProfile — Zernio assigns the next slot in that tenant's queue |
Firing publishNow for all tenants on the hour | Let each tenant's queue slots spread the load |
| A create-post loop for a tenant's CSV import | POST /v1/posts/bulk-upload — one call, with dryRun to validate first |
For posts that must go out at an exact moment (a launch, a drop), scheduledFor with a specific time is still right — the anti-pattern is everything publishing on round hours.
Outcomes come from webhooks
The create response means accepted — status: "scheduled" and the assigned scheduledFor. What actually happened at publish time arrives on the post webhooks, per platform:
Route events to the tenant via the accountId on each platform entry, then update your UI from the post's status:
| Status | Meaning | Show the tenant |
|---|---|---|
scheduled | Waiting for its slot | The scheduled time |
publishing | Being delivered right now (transient) | A spinner |
published | Every platform succeeded | Live post URLs |
partial | Some platforms succeeded, some failed | Which succeeded, which failed, per-platform errors |
failed | Every platform failed | The error, with a retry action |
post.platform.published / post.platform.failed fire per platform as results land, so multi-platform posts can update progressively instead of waiting for the aggregate. The post lifecycle guide covers every transition.
Checklist
- Only pass account IDs from the tenant's own profile — isolation on create is your responsibility
- Fresh
x-request-idper logical post; same UUID on retries;200+existingPostand409both mean "already done" - One media upload per asset, reused across platforms; presign near publish time (7-day temp window)
- Per-tenant queues (
queuedFromProfile) instead of everything at :00;bulk-uploadfor imports - Status from
post.published/post.partial/post.failedwebhooks, routed byaccountId— not from polling
Related
- Multi-tenant integration - the core profile-per-customer model
- Idempotency & safe retries - both dedup layers in depth
- Queue scheduling - slots, multiple queues, previewing the next slot
- Media uploads - presigned URLs, size limits, per-platform requirements
- Post lifecycle - every status and transition
Build a Platform
Model each of your customers as a profile, connect their accounts, scope API keys per tenant, and route webhooks back to the right customer.
Tenant Analytics
Show each of your customers their own social analytics - profileId filters, the tiered sync-worker pattern, request budgets, and data freshness expectations.