How to Post to Facebook Pages via Graph API: A Developer's Guide to Automated Publishing

A comprehensive guide to Facebook's Graph API for Page publishing — permissions, token lifecycle, text posts, photos, videos, Reels, rate limits, and the app review maze.

How to Post to Facebook Pages via Graph API: A Developer's Guide to Automated Publishing

What you can and cannot do

Facebook’s Graph API lets you publish to Pages — text posts, photos, videos, links, Reels, and stories. It does not let you post to personal profiles. Facebook removed personal profile posting from the API after Cambridge Analytica (publish_actions was deprecated in Graph API v3.0 in 2018). Every publishing operation in this guide targets a Facebook Page.

The current API version is v25.0 (released February 2026). Meta releases new versions roughly twice a year, with each version supported for approximately two years.

The permission maze

This is where most developers get stuck. Facebook’s permission model has three layers, and you need to clear all of them before your app can publish in production.

Layer 1: Permissions

Your app needs these permissions to publish to Pages:

PermissionWhat it doesDependencies
pages_show_listAccess list of Pages the user managesNone
pages_read_engagementRead Page content and metadatapages_show_list
pages_manage_postsCreate, edit, and delete Page postspages_read_engagement, pages_show_list
pages_manage_metadataUpdate Page settings, subscribe to webhookspages_show_list
pages_manage_engagementManage comments and likespages_read_user_content, pages_show_list

For basic publishing (text, photos, videos, links), you need pages_manage_posts and its dependencies: pages_read_engagement and pages_show_list. That is the minimum.

Layer 2: App review

Every permission beyond public_profile and email requires formal app review. Each permission needs its own submission with:

  • A detailed written justification explaining why your app needs this specific permission
  • A screencast video showing the complete user flow for that permission in your app
  • A working, accessible privacy policy
  • A test account the reviewer can log into

The #1 reason for app review rejection is vague justifications. “To improve user experience” will be rejected. Be specific: “Our app allows marketing teams to schedule and publish posts to their Facebook Page from a content calendar. pages_manage_posts is required to create posts on the Page when the scheduled time arrives.”

Other common rejection reasons:

  • Screencast does not show the full user journey
  • App is inaccessible to reviewers (broken login, missing test credentials)
  • Requesting permissions you have not implemented yet
  • Using fake accounts (auto-rejection)

Layer 3: Business verification

Apps requesting Advanced Access permissions must complete Business Verification through Meta Business Manager. This involves verifying your legal business entity — business documents, domain verification, and sometimes a phone call from Meta.

Advanced Access is required if your app publishes on behalf of Facebook Pages you do not own or manage.

Token lifecycle

Facebook’s token system is the second most common source of developer frustration after app review. Here is how it works.

Token types

TokenLifetimeHow to get it
Short-lived user token1–2 hoursFacebook Login dialog
Long-lived user token~60 daysExchange from short-lived token
Page token (from short-lived user token)1–2 hoursGET /{user-id}/accounts
Page token (from long-lived user token)Never expiresGET /{user-id}/accounts

For automated publishing, you need a never-expiring Page token. Here is the flow:

Getting a never-expiring Page token

Step 1 — Get a short-lived user token through Facebook Login with the required permissions.

Step 2 — Exchange for a long-lived user token:

GET /oauth/access_token
?grant_type=fb_exchange_token
&client_id={app-id}
&client_secret={app-secret}
&fb_exchange_token={short-lived-token}

Returns a token valid for approximately 60 days.

Step 3 — Get the Page token using the long-lived user token:

GET /{user-id}/accounts

Each Page in the response includes an access_token. When this request is made with a long-lived user token, the returned Page tokens never expire.

Step 4 — Verify in the Access Token Debugger. The “Expires” field should show “Never”.

When “never expires” stops being true

Even non-expiring Page tokens are invalidated if:

  • The user changes their Facebook password
  • The user de-authorizes your app
  • The user loses admin access to the Page
  • You reset your app secret

Your app needs to handle token invalidation gracefully and prompt re-authentication when needed.

POST to /{page-id}/feed:

{
"message": "Your post text",
"link": "https://example.com/article",
"access_token": "<PAGE_ACCESS_TOKEN>"
}

If you include a link, Facebook auto-generates a link preview card. The message parameter is the text that appears above the card.

Scheduling posts

Set published to false and provide a scheduled_publish_time:

{
"message": "Launches tomorrow at 9am ET",
"published": false,
"scheduled_publish_time": 1710324000,
"access_token": "<PAGE_ACCESS_TOKEN>"
}

The scheduled_publish_time must be between 10 minutes and 30 days from the request time. It accepts:

  • Integer UNIX timestamp in seconds
  • ISO 8601 timestamp string
  • PHP strtotime() parsable strings (e.g., +2 weeks, tomorrow)

Geographic targeting

You can restrict post visibility by geography:

{
"message": "Grand opening in Vancouver",
"targeting": {
"geo_locations": {
"countries": ["CA"],
"cities": [{"key": "296875"}]
}
}
}

Publishing photos

POST to /{page-id}/photos:

{
"url": "https://example.com/photo.jpg",
"message": "Photo caption",
"access_token": "<PAGE_ACCESS_TOKEN>"
}

The photo must be at a publicly accessible URL. Returns both a photo_id and a post_id.

Publishing videos

Videos use the Resumable Upload API, which is a three-step process:

Step 1 — Start upload session

POST to /{app-id}/uploads:

{
"file_name": "video.mp4",
"file_length": 12345678,
"file_type": "video/mp4",
"access_token": "<PAGE_ACCESS_TOKEN>"
}

Returns: {"id": "upload:<UPLOAD_SESSION_ID>"}

Step 2 — Upload the file

POST to /upload:<UPLOAD_SESSION_ID> with headers:

Authorization: OAuth <PAGE_ACCESS_TOKEN>
file_offset: 0

Send the video binary in the request body.

Returns: {"h": "<UPLOADED_FILE_HANDLE>"}

If the upload is interrupted, GET /upload:<UPLOAD_SESSION_ID> returns the current file_offset. Resume by POSTing with the new offset.

Step 3 — Publish

POST to /{page-id}/videos:

{
"title": "Video title",
"description": "Video description",
"fbuploader_video_file_chunk": "<UPLOADED_FILE_HANDLE>",
"access_token": "<PAGE_ACCESS_TOKEN>"
}

Returns: {"id": "<VIDEO_ID>"}

Video limits: 1 GB / 20 minutes for single-request uploads, 1.5 GB / 45 minutes for resumable uploads.

Publishing Reels

Facebook Reels have their own three-step flow, separate from regular video uploads.

Video specs for Reels

SpecRequirement
Duration3–60 seconds
FormatMP4 only
ResolutionMinimum 1080p input (capped at 720p after upload)
Aspect ratio9:16 (vertical)
Frame rate23 FPS minimum

Step 1 — Initialize upload

POST to /{page-id}/video_reels:

{
"upload_phase": "start",
"access_token": "<PAGE_ACCESS_TOKEN>"
}

Returns a video_id and upload_url.

Step 2 — Upload video

POST to https://rupload.facebook.com/video-upload/{video-id}:

Authorization: OAuth <PAGE_ACCESS_TOKEN>
file_url: https://example.com/reel.mp4

Note the different host — Reels uploads go to rupload.facebook.com, not graph.facebook.com.

Step 3 — Publish

POST to /{page-id}/video_reels:

{
"upload_phase": "finish",
"video_id": "<VIDEO_ID>",
"video_state": "PUBLISHED",
"description": "Reel caption",
"title": "Reel title",
"access_token": "<PAGE_ACCESS_TOKEN>"
}

video_state accepts PUBLISHED, DRAFT, or SCHEDULED. For scheduled Reels, include scheduled_publish_time.

Reels limitations

  • Reels can only be posted to Facebook Pages — not personal profiles or Groups
  • The video_reels endpoint does not support reads, updates, or deletes
  • This is a separate endpoint from Instagram Reels — publishing a Reel to Facebook does not cross-post it to Instagram

Rate limits

Facebook’s rate limiting is formula-based, not a fixed number:

App-level (user access tokens): 200 × (number of app users) calls per rolling one-hour window. This is aggregate across your entire app, not per-user.

Pages API (page/system tokens): 4,800 × (number of engaged users) calls per 24 hours. “Engaged users” means users who have interacted with the Page.

Monitoring headers

Every API response includes usage headers:

  • X-App-Usage — contains call_count, total_cputime, total_time as percentages
  • X-Business-Use-Case-Usage — same metrics plus estimated_time_to_regain_access in minutes

Throttling triggers when any metric hits 100%.

Rate limit error codes

CodeMeaning
4App-level rate limit
17User-level rate limit
32Pages API rate limit
80001Page account limit
613Reels-specific rate limit

When you hit a rate limit, implement exponential backoff (1s, 2s, 4s, etc.) and distribute requests evenly over time rather than bursting.

API versioning

Meta releases new Graph API versions roughly twice a year. Each version is supported for approximately two years. As of March 2026:

VersionDeprecation date
v22.0February 19, 2026
v23.0June 9, 2026
v24.0TBD
v25.0 (current)TBD

Unversioned calls default to the oldest available version. Always specify a version in your API calls to avoid breaking changes.

Post management

Once published, you can manage Page posts through the API:

  • Read posts: GET /{page-id}/feed
  • Update a post: POST /{page-post-id} with updated message
  • Delete a post: DELETE /{page-post-id}

These all require the pages_manage_posts permission and a valid Page access token.

The same post, through Postproxy

Here is what publishing to a Facebook Page looks like through Postproxy:

Terminal window
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": {
"body": "Behind the scenes of our latest product shoot"
},
"profiles": ["facebook"],
"media": ["https://example.com/video.mp4"]
}'

One request. Postproxy determines the right endpoint (feed, photos, videos, or Reels), handles the resumable upload session, manages the Page access token, and returns the result.

If you want the same content on Facebook, Instagram, and LinkedIn:

Terminal window
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": {
"body": "Behind the scenes of our latest product shoot"
},
"profiles": ["facebook", "instagram", "linkedin"],
"media": ["https://example.com/video.mp4"]
}'

Postproxy handles the Graph API for Facebook, the container model for Instagram, and LinkedIn’s different upload protocol — all from one request.

What Postproxy handles

Postproxy manages the Graph API complexity so your system does not have to:

  • Business verification and app review already completed
  • All required permissions approved and maintained
  • Page access token exchange, refresh, and invalidation handling
  • Content type detection and routing to the correct endpoint
  • Resumable video uploads with retry logic
  • Reels three-step upload flow via rupload.facebook.com
  • Rate limit monitoring via usage headers
  • API version management across releases
  • Per-platform error reporting with actionable failure reasons

Your system sends content. Postproxy handles the Facebook-specific implementation.

Connect your Facebook Page and start publishing through the Postproxy API.

Ready to get started?

Start with our free plan and scale as your needs grow. No credit card required.