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.
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:
| Permission | What it does | Dependencies |
|---|---|---|
pages_show_list | Access list of Pages the user manages | None |
pages_read_engagement | Read Page content and metadata | pages_show_list |
pages_manage_posts | Create, edit, and delete Page posts | pages_read_engagement, pages_show_list |
pages_manage_metadata | Update Page settings, subscribe to webhooks | pages_show_list |
pages_manage_engagement | Manage comments and likes | pages_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
| Token | Lifetime | How to get it |
|---|---|---|
| Short-lived user token | 1–2 hours | Facebook Login dialog |
| Long-lived user token | ~60 days | Exchange from short-lived token |
| Page token (from short-lived user token) | 1–2 hours | GET /{user-id}/accounts |
| Page token (from long-lived user token) | Never expires | GET /{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}/accountsEach 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.
Publishing text and link posts
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: 0Send 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
| Spec | Requirement |
|---|---|
| Duration | 3–60 seconds |
| Format | MP4 only |
| Resolution | Minimum 1080p input (capped at 720p after upload) |
| Aspect ratio | 9:16 (vertical) |
| Frame rate | 23 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.mp4Note 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_reelsendpoint 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— containscall_count,total_cputime,total_timeas percentagesX-Business-Use-Case-Usage— same metrics plusestimated_time_to_regain_accessin minutes
Throttling triggers when any metric hits 100%.
Rate limit error codes
| Code | Meaning |
|---|---|
| 4 | App-level rate limit |
| 17 | User-level rate limit |
| 32 | Pages API rate limit |
| 80001 | Page account limit |
| 613 | Reels-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:
| Version | Deprecation date |
|---|---|
| v22.0 | February 19, 2026 |
| v23.0 | June 9, 2026 |
| v24.0 | TBD |
| 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 updatedmessage - 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:
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:
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.
Postproxy
One API for every social platform
Publish to Instagram, X, LinkedIn, TikTok, YouTube and more with a single request. Free plan, no credit card required.