Quick reference
| Field | Value |
|---|
| Caption limit | 63,206 characters |
| Photos per post | 1–10 |
| Videos per post | 1 |
| Mixed media | ❌ Not on organic Page posts |
| Image formats | JPG, PNG, GIF, WebP, HEIC, HEIF |
| Video formats | MP4, MOV, AVI, WebM |
| Max image size | 5 GB |
| Max video size | 5 GB |
| Reel duration | 3–90 seconds |
| Feed video duration | up to 240 minutes |
| Reels per Page | 30 per 24 hours |
| Post types | Feed, Photo, Photo Carousel, Video, Reel |
| First comment | ✅ Auto-posts after publish (not on Reels) |
| Account types | Facebook Pages (not personal profiles) |
The image/video size caps above are Postbreeze’s server-side ingest
limits. Facebook’s own publish-time caps are tighter (Reels must be
9:16 and 3–90s, max 30 Reels per Page per 24h, etc.) — exceed those
and the publish step returns an error from Meta even though the
upload succeeded.
Before you start
You can only publish to Facebook Pages you administer. Personal-profile publishing isn’t exposed by the Graph API. The OAuth flow enumerates the Pages on your account during connect — pick the Page you want to publish to from that list.
Required scopes:
pages_show_list — read the list of Pages you administer.
pages_manage_posts — create posts on your Pages.
pages_manage_engagement + pages_read_engagement — first-comment + inbox replies.
pages_read_user_content — inbox comments-on-your-posts feed.
Reels require Meta App Review with the Reels use-case enabled. Before review approval, video posts work fine but kind: "REEL" will return FB_REEL_NOT_APPROVED.
Quick start
Workspace is inferred from your API key — no workspaceId argument
and no workspace in the URL. Use the flat shape (content +
platforms) for the common case; drop to the
nested shape at the bottom of this page
only when a power-user workflow demands it.
import Postbreeze from "@postbreeze/node";
const postbreeze = new Postbreeze({ apiKey: process.env.POSTBREEZE_API_KEY });
const post = await postbreeze.posts.create({
content: "Our new feature drop — read more 👇",
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
platforms: [{
accountId: "soc_facebook_…",
platformOptions: {
platform: "FACEBOOK_PAGE",
kind: "FEED",
link: "https://example.com/blog/new-feature",
},
}],
});
See Platform settings → Facebook for
the full platformOptions reference and
Media uploads for the two ways to attach
images and video.
Content types
Feed (text + optional link)
kind: "FEED". Pure text post, optionally with a link whose OG
preview Facebook auto-generates. When you attach media to a FEED
post, the server auto-normalizes it to PHOTO (1 image) or
PHOTO_CAROUSEL (2–10) — you don’t have to set kind yourself.
Photo (single image)
kind: "PHOTO". Send exactly one image, either by mediaIds (the
presign flow) or by mediaItems (URL ingest).
Node.js — pre-uploaded media
await postbreeze.posts.create({
content: "New drop today",
mediaIds: ["med_abc123"],
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
platforms: [{
accountId: "soc_facebook_…",
platformOptions: { platform: "FACEBOOK_PAGE", kind: "PHOTO" },
}],
});
Node.js — URL ingest (alternative)
await postbreeze.posts.create({
content: "New drop today",
mediaItems: [
{ type: "image", url: "https://cdn.example.com/drop.jpg", altText: "Product hero shot" },
],
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
platforms: [{
accountId: "soc_facebook_…",
platformOptions: { platform: "FACEBOOK_PAGE", kind: "PHOTO" },
}],
});
mediaItems accepts any reachable URL — Postbreeze fetches the asset
through an SSRF-guarded ingest path. Use mediaIds when you’ve already
uploaded the file via the
presign flow.
Photo Carousel (2–10 images)
kind: "PHOTO_CAROUSEL". Mixed photo + video is not allowed on organic Page posts — that surface is ads-only.
const post = await postbreeze.posts.create({
content: "Highlights from the event",
mediaIds: ["med_1", "med_2", "med_3", "med_4", "med_5"],
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
platforms: [{
accountId: "soc_facebook_…",
platformOptions: {
platform: "FACEBOOK_PAGE",
kind: "PHOTO_CAROUSEL",
},
}],
});
Video
kind: "VIDEO". Single video file. Up to 4 hours of runtime; the
server-side ingest cap is 5 GB.
await postbreeze.posts.create({
content: "Behind the scenes of the launch",
mediaIds: ["med_video_abc"],
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
platforms: [{
accountId: "soc_facebook_…",
platformOptions: { platform: "FACEBOOK_PAGE", kind: "VIDEO" },
}],
});
Reel
kind: "REEL". 9:16, 3–90 seconds, video-only. Optionally pass
reelTitle — it’s prepended to the description on the Reel surface
(not the Feed).
await postbreeze.posts.create({
content: "How we shipped this in a week",
mediaIds: ["med_reel_abc"],
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
platforms: [{
accountId: "soc_facebook_…",
platformOptions: {
platform: "FACEBOOK_PAGE",
kind: "REEL",
reelTitle: "Shipping in a week",
},
}],
});
Images
| Property | Requirement |
|---|
| Max items | 10 per carousel, 1 per Photo post |
| Formats | JPG, PNG, GIF, WebP, HEIC, HEIF |
| Max file size | 5 GB (server-side ingest) |
| Aspect ratio | Any (Facebook crops to 1.91:1 in Feed by default) |
| Min resolution | 600 × 315 |
Videos
| Property | Requirement |
|---|
| Formats | MP4, MOV, AVI, WebM |
| Max file size | 5 GB (server-side ingest) |
| Max duration (Feed) | 240 minutes |
| Max duration (Reel) | 90 seconds |
| Min duration (Reel) | 3 seconds |
| Aspect ratio (Feed) | 16:9 to 1:1 |
| Aspect ratio (Reel) | 9:16 |
| Codecs | H.264 + AAC |
| Field | Type | Required | Description |
|---|
platform | "FACEBOOK_PAGE" | Yes | Discriminator. |
kind | enum | No (default FEED) | FEED, PHOTO, PHOTO_CAROUSEL, VIDEO, or REEL. With media attached, FEED auto-normalizes to PHOTO / PHOTO_CAROUSEL. |
link | URL | No | Outbound link. Auto-generates a preview card when kind: "FEED". |
reelTitle | string | No | Reel-only title prepended to the description (≤ 2,200 chars). |
That’s the complete schema — there is no pageId, geoRestriction,
or story support on Facebook today. The firstComment field is not
inside platformOptions; it lives one level up as a sibling of
platformOptions on each platforms entry (see below).
Pass firstComment on the platforms entry — sibling of
platformOptions, not nested inside it. Facebook publishes the comment
as the Page (not as a personal user). No delay needed — Facebook
accepts immediate comments after publish.
await postbreeze.posts.create({
content: "Our new feature drop",
scheduledFor: new Date(Date.now() + 30_000).toISOString(),
platforms: [{
accountId: "soc_facebook_…",
platformOptions: { platform: "FACEBOOK_PAGE", kind: "FEED" },
firstComment: "More details on our blog → https://example.com/blog/new-feature",
}],
});
Limit: 8,000 characters per Page comment. First comments are supported
on every Facebook surface except Reels — Meta’s Reels API rejects
programmatic first comments.
Analytics
| Metric | Available |
|---|
| Page follows | ✅ |
| Page media views | ✅ |
| Page post engagements | ✅ |
| Page video views | ✅ |
| Page views total | ✅ |
| Reach | ✅ |
| Reactions breakdown | ✅ |
| Post clicks | ✅ |
| Comments | ✅ |
| Shares | ✅ |
Refresh cadence: every 14 days.
The 2026 metric names above (page_follows, page_media_view, etc.) replaced the pre-June-2026 page_impressions family. Postbreeze emits the new names directly — no migration needed.
Common errors
| Error | Meaning | Fix |
|---|
FB_PAGE_TOKEN_EXPIRED | The Page access token is invalid | Reconnect the account. |
FB_REEL_NOT_APPROVED | Your Meta App hasn’t been reviewed for the Reels use-case | Submit for Meta App Review. |
FB_MIXED_MEDIA | Photos + videos in mediaIds | Pick one type. |
FB_CAROUSEL_SIZE | Carousel had fewer than 2 or more than 10 photos | Trim to 2–10. |
FB_RATE_LIMITED | Page or app rate limit hit | Postbreeze retries with backoff. |
FB_LINK_INVALID | The link URL didn’t resolve | Verify the URL works in an incognito browser. |
FB_VIDEO_TOO_LARGE | Video > 5 GB | Compress before uploading. |
FB_REEL_QUOTA | More than 30 Reels published on this Page in 24h | Wait for the rolling window to clear; Postbreeze retries with backoff. |
What you can’t do
- ❌ Publish to personal Facebook profiles
- ❌ Schedule via Facebook’s native scheduler (we use our own queue)
- ❌ Mix video and image in one carousel (ads-only feature)
- ❌ Live video / Live Reels
- ❌ Stories (planned for v2)
- ❌ Tag specific users in caption text
- ❌ Add location tags (Meta removed this from the Graph API)
- ❌ Cross-post to Instagram in the same call (use two entries in
platforms)
- ❌ First comment on Reels (Meta API limitation)
Full control: nested shape
The flat shape (content + platforms) above is the canonical entry
point and covers every Facebook use case. Postbreeze also accepts an
equivalent nested shape (caption + targets) for callers porting
from older internal tooling — the two shapes produce identical
posts.
import Postbreeze from "@postbreeze/node";
const postbreeze = new Postbreeze({ apiKey: process.env.POSTBREEZE_API_KEY });
const post = await postbreeze.posts.create({
caption: "Our new feature drop — read more 👇",
scheduledAt: new Date(Date.now() + 30_000).toISOString(),
mediaIds: ["med_hero_image"],
targets: [{
socialAccountId: "soc_facebook_…",
platformOptions: {
platform: "FACEBOOK_PAGE",
kind: "PHOTO",
link: "https://example.com/blog/new-feature",
},
firstComment: "More details on our blog →",
}],
});
Field mapping flat ↔ nested:
| Flat | Nested |
|---|
content | caption |
scheduledFor | scheduledAt |
platforms[] | targets[] |
platforms[].accountId | targets[].socialAccountId |
mediaItems / mediaIds | mediaIds (URL ingest is flat-only) |
Prefer the flat shape for all new integrations.