List posts
Returns a paginated list of posts. Published posts include platformPostUrl with the public URL on each platform. A query parameter that is not listed here returns 400 naming it and the accepted parameters, so a misspelled filter never silently returns the unfiltered list.
Authorization
bearerAuth API key authentication: send your Zernio API key in the Authorization header, prefixed with Bearer.
In: header
Query Parameters
Page number (1-based)
1 <= value1Page size. Values above the maximum return 400 rather than being clamped.
1 <= value <= 50010Row offset. Takes precedence over page when both are sent; the response pagination.page is derived from it.
0 <= valueOpaque cursor from a previous response's pagination.nextCursor. Returns the page that follows it in the same sortBy order, and takes precedence over page and offset. Each cursor page costs the same however deep it is, unlike page/offset, which get slower the further you go; prefer it to walk a long history. Send the same filters and sortBy as the request that issued it: a cursor from another sortBy returns 400. Not available with sortBy=status or sortBy=platform, or with source=external.
Comma-separated list of post fields to return. Each post then carries _id plus only these fields, which makes large pages much cheaper: leave out platforms, content, mediaItems or metadata when you do not need them. Accepted: userId, title, content, mediaItems, platforms, scheduledFor, timezone, status, tags, hashtags, mentions, visibility, metadata, recycling, recycledFromPostId, queuedFromProfile, queueId, createdAt, updatedAt. An unknown field returns 400 naming it. fields=_id returns ids only. Omit it (or send it empty) for the full post. Not available with source=external.
Only posts whose updatedAt is at or after this ISO 8601 timestamp, an offset or Z required. updatedAt moves when the post or any of its platforms changes (an edit, a status change, a platform publishing or failing), so to keep a copy in sync, send the time of your previous sync instead of reading every page again. Combines with the other filters and with cursor. Deleted posts are not returned. Not available with source=external.
date-timeWhich collection to read. zernio (default) returns posts authored through Zernio. external returns posts synced from the platform (existing/historical posts that were published outside Zernio). Combine with accountId and paginate via page/limit to walk the full synced history (we keep up to the last ~12 months per account).
"zernio"Value in
- "zernio"
- "external"
Value in
- "draft"
- "pending_approval"
- "scheduled"
- "publishing"
- "published"
- "partial"
- "failed"
- "cancelled"
Filter posts to a specific profile (24-char hex ObjectId). Omit it, or send all or an empty value, to list posts across every profile.
Filter posts to those created by a specific team user (24-char hex ObjectId).
Zero-padded YYYY-MM-DD, or a full ISO 8601 datetime. An empty value means no date filter; any other malformed value returns 400. The same name the other date-window filters use (ads, analytics).
dateZero-padded YYYY-MM-DD, or a full ISO 8601 datetime. An empty value means no date filter; any other malformed value returns 400. The same name the other date-window filters use (ads, analytics).
dateAlias of fromDate, kept for existing callers
dateAlias of toDate, kept for existing callers
dateSearch posts by text content.
Sort order for results.
"scheduled-desc"Value in
- "scheduled-desc"
- "scheduled-asc"
- "created-desc"
- "created-asc"
- "status"
- "platform"
Filter posts to those published via a specific account (24-char hex ObjectId).
Response Body
application/json
application/json
application/json
import Zernio from '@zernio/node';const zernio = new Zernio({ apiKey: process.env.ZERNIO_API_KEY });const { data } = await zernio.posts.listPosts();console.log(data);{ "posts": [ { "_id": "65f1c0a9e2b5af0012ab34cd", "title": "Launch post", "content": "We just launched!", "status": "scheduled", "scheduledFor": "2024-11-01T10:00:00Z", "timezone": "UTC", "platforms": [ { "platform": "twitter", "accountId": { "_id": "64e1f0...", "platform": "twitter", "username": "@acme", "displayName": "Acme Corp", "isActive": true }, "status": "pending" } ], "tags": [ "launch" ], "createdAt": "2024-10-01T12:00:00Z", "updatedAt": "2024-10-01T12:00:00Z" } ], "pagination": { "page": 1, "limit": 10, "total": 1, "pages": 1 }}Update an OAuth app
Renames the app or rotates its secret. A new secret is checked with the platform before it is saved, and takes effect for every account on the app at once; their tokens keep working. The client ID cannot change: a different client ID is a different app, so add it and reconnect the accounts through it.
Create post
Create a post, and optionally publish it in the same request. A post published immediately (`publishNow: true`) comes back with `platformPostUrl` in the response. `content` is optional in four cases: - media is attached - all platforms have `customContent` - every platform entry is an X Article (`platformSpecificData.article`) - every platform entry is a LinkedIn text-free reshare (`platformSpecificData.reshareUrl` with no text) See each platform's schema for media constraints. ## LinkedIn @mentions Put the profile or company URL inside `@[...]` in `content`, or in a LinkedIn entry's `customContent`: `@[https://www.linkedin.com/in/jane-doe]` or `@[https://www.linkedin.com/company/acme]`. The post is published with LinkedIn's real name and link, and no lookup call is needed first. Custom text is optional, as in `@[Jane](https://www.linkedin.com/in/jane-doe)`: for a person it must be the first name, last name or full name, for a company the exact company name, both case-sensitive. Any other text is replaced with LinkedIn's name and a `warnings` entry is returned (`linkedin_mention_name_replaced`). `@[Text](urn:li:person:ID)` markup, as returned by `GET /v1/accounts/{accountId}/linkedin-mentions`, is published exactly as you wrote it: it is not looked up, checked or changed, so its text must already be LinkedIn's name. Resolving a person's profile URL needs one of your LinkedIn accounts to administer a Page; publishing does not. Only profile and company URLs can fail. A scheduled or published post is rejected when one cannot be resolved: `400` with code `linkedin_mentions_invalid`, `429` with `rate_limited` (LinkedIn is rate limiting), `401` with `reconnect_required` (expired token) or a retryable `503` with `temporarily_unavailable` (LinkedIn did not answer within 8 seconds in total). Each failing mention is listed under `details.mentions[]` (`LinkedInMentionIssue`) with its own code: `linkedin_mention_unresolvable`, `linkedin_mention_requires_page_admin` or `linkedin_mention_name_unavailable` (LinkedIn withheld the name and none was given). A post can resolve at most 10 profile or company URL mentions across its shared content and LinkedIn entries: more is a `400` with `linkedin_mentions_limit_exceeded`. Drafts never fail on a mention: unresolved ones (and more than 10) stay as written and come back as `warnings`. A draft is checked again when it is scheduled. On platforms other than LinkedIn, `@[linkedin.com/in/jane]` is posted as its text, or as the vanity (`jane`) when you wrote no text, and `@[Text](urn:li:...)` as `Text`. ## Scheduling Pick one of: - `scheduledFor`: publish at the scheduled time - `publishNow: true`: publish synchronously, inside this request - `queuedFromProfile`: publish in the profile's next queue slot With none of them and `isDraft` unset, the post is saved as a draft. `platforms` is required unless the post is a draft. Precedence: `isDraft: true` wins over `publishNow` and `scheduledFor` (the post is saved, never published), and `publishNow: true` wins over `scheduledFor`. A `scheduledFor` already in the past is not rejected: the post is published synchronously in the same request, exactly like `publishNow`. ## Idempotency Three layers of duplicate-protection apply, so safe-to-retry callers (network blips, n8n / Zapier retries, etc.) don't accidentally double-post. **1. `Idempotency-Key` (recommended for retries, 24-hour window).** Send an `Idempotency-Key` header (a UUID per logical post, up to 255 characters) and reuse it when you retry after a 5xx, a timeout or a dropped connection. A retry with the same key within 24 hours returns **HTTP 200** with the original post in the `post` field, and no new post is created. The match is on the key alone, not on the body: a retry returns the original post even if its body differs (for example a re-uploaded media URL). It covers drafts, and a post that was saved even though the original response was a 5xx or timed out. If the first request is still being processed, the retry gets **HTTP 409** with `code: idempotency_conflict` and a `Retry-After` header; retry after that delay. If the first request failed without creating a post, the retry is processed as a new request. Keys are scoped to your user. Never reuse a key for a different post: that post would not be created, and the earlier one would be returned instead. **2. `x-request-id` retry detection (24-hour window).** If a request carries the same `x-request-id` as an earlier one AND collides with it on the content fingerprint below (same account, content and media URLs), we return **HTTP 200** with the original post in the `post` field (or **HTTP 202** with `postId` if the original is still being saved) instead of a 409. A request with the same `x-request-id` but different content is a new post. When both headers are sent, `Idempotency-Key` wins and `x-request-id` is ignored for matching. The official Zernio SDKs auto-generate a unique `x-request-id` per call. On a generic HTTP client (curl, n8n's HTTP node, Zapier, custom code), either: - Set a unique `x-request-id` per logical call (UUIDv4 is fine) - Or omit the header, and we'll treat each request as new **3. Content-hash dedup (24-hour window).** Independently, we hash `(platform, accountId, content + media URLs)` and reject duplicates within 24 hours with **HTTP 409**. This catches genuine "same content posted twice to the same account" cases regardless of `x-request-id`. The response carries `error`, `accountId`, `platform`, and `existingPostId` so you can find the original. To intentionally re-post identical content within 24h, change something (the caption, the media, the account), because the dedup is keyed on the full content fingerprint. Order: an `Idempotency-Key` match (200) is checked first, then same-`x-request-id` retries (200), then the content-hash dedup (409).