Use the Facebook Graph API, store each post ID, and run a scheduled Python job that refreshes reactions, comments, shares, and impressions for posts that are already published. That is the cleanest way to keep reporting accurate without asking a marketer to export CSV files every morning. The process is not complex, but permissions, rate limits, and metric names can waste time if you treat them casually.

TLDR: Create a Meta app, get a Page access token, pull published post IDs, then use Python to request engagement metrics from the Graph API and update your database or spreadsheet. For example, a retail team with 240 Facebook posts found that engagement totals changed by 18% over the first 72 hours after publishing, so daily refreshes made their campaign reports far more accurate. Run the script on a schedule, log every API response, and keep raw values before calculating rates. If a post has 1,200 reactions today and 1,530 tomorrow, your script should update the stored value while preserving the timestamp.

What You Need Before Writing Python Code

Before you make any API calls, set up access correctly. Facebook data access is strict, and some metrics need approved permissions. Honestly, it feels like a simple report should not require this many screens, but skipping this setup leads to broken scripts later.

  • A Meta developer app connected to your business account.
  • A Facebook Page where you have the right role, usually admin or analyst access.
  • A Page access token with permissions such as pages_read_engagement and pages_read_user_content.
  • Published post IDs, either already stored or fetched from the Page feed endpoint.
  • A destination, such as PostgreSQL, BigQuery, Google Sheets, Airtable, or a CSV file.

For production work, avoid short lived tokens. Use a long lived token where allowed, and store it in an environment variable or secret manager. Do not paste tokens into source code. That mistake turns a small reporting task into a security problem.

Which Engagement Fields Can You Update?

For published Facebook Page posts, engagement data often includes:

  • Reactions, including total reaction count and sometimes reaction types.
  • Comments, usually total count.
  • Shares, if available for the post type and permission level.
  • Post impressions, such as total impressions or unique impressions.
  • Clicks, depending on the post format and metric access.
  • Video metrics, such as views and average watch time, for video posts.

The available fields can differ by API version, Page settings, post type, and permissions. It drives me crazy that a metric can work for one post and return nothing for another, but that is normal with social APIs. Your code should expect missing fields and continue safely.

Basic Python Setup

Install the libraries you need. For a simple script, requests is enough. For scheduled reporting, add a database connector and structured logging.

pip install requests python-dotenv

Keep your token outside the script:

FACEBOOK_PAGE_ACCESS_TOKEN=your_token_here
FACEBOOK_PAGE_ID=your_page_id_here

Then load it in Python:

import os
import requests
from datetime import datetime, timezone
from dotenv import load_dotenv

load_dotenv()

ACCESS_TOKEN = os.getenv("FACEBOOK_PAGE_ACCESS_TOKEN")
PAGE_ID = os.getenv("FACEBOOK_PAGE_ID")
API_VERSION = "v19.0"
BASE_URL = f"https://graph.facebook.com/{API_VERSION}"

Fetch Published Posts

If you already store post IDs when content is published, use those. That is the best option. If not, fetch recent posts from the Page feed and save their IDs.

def get_published_posts(limit=25):
    url = f"{BASE_URL}/{PAGE_ID}/posts"
    params = {
        "access_token": ACCESS_TOKEN,
        "limit": limit,
        "fields": "id,message,created_time,permalink_url"
    }

    response = requests.get(url, params=params, timeout=30)
    response.raise_for_status()
    return response.json().get("data", [])

This function returns published Page posts. Store the id, created_time, and permalink_url. The post ID is your stable key for future refreshes.

Request Engagement Data for Each Post

You can request basic engagement fields from the post object. For many Page reports, reactions.summary(true), comments.summary(true), and shares are useful starting points.

def get_post_engagement(post_id):
    url = f"{BASE_URL}/{post_id}"
    fields = (
        "id,"
        "created_time,"
        "permalink_url,"
        "reactions.summary(true).limit(0),"
        "comments.summary(true).limit(0),"
        "shares"
    )

    params = {
        "access_token": ACCESS_TOKEN,
        "fields": fields
    }

    response = requests.get(url, params=params, timeout=30)
    response.raise_for_status()
    data = response.json()

    reactions = data.get("reactions", {}).get("summary", {}).get("total_count", 0)
    comments = data.get("comments", {}).get("summary", {}).get("total_count", 0)
    shares = data.get("shares", {}).get("count", 0)

    return {
        "post_id": data.get("id"),
        "permalink_url": data.get("permalink_url"),
        "reactions": reactions,
        "comments": comments,
        "shares": shares,
        "engagement_total": reactions + comments + shares,
        "updated_at": datetime.now(timezone.utc).isoformat()
    }

This gives you a clean record that can be used to update a table. Keep engagement_total as a calculated field, not your only stored value. Separate values make audits easier.

Update Your Storage Layer

The update method depends on where your reporting data lives. A database is better than a spreadsheet for repeatable reporting. Still, a spreadsheet may be fine for small teams.

A reliable table might use these columns:

  • post_id: unique Facebook post ID.
  • permalink_url: direct link to the post.
  • reactions: latest reaction count.
  • comments: latest comment count.
  • shares: latest share count.
  • engagement_total: reactions plus comments plus shares.
  • updated_at: time of the latest successful refresh.

Use an upsert pattern. That means the script inserts a record if the post is new, or updates the existing row if the post ID already exists. This prevents duplicates and keeps reporting clean.

def refresh_engagement_for_posts(post_ids):
    results = []

    for post_id in post_ids:
        try:
            engagement = get_post_engagement(post_id)
            results.append(engagement)
            print(f"Updated {post_id}: {engagement['engagement_total']} engagements")
        except requests.HTTPError as error:
            print(f"API error for {post_id}: {error}")
        except Exception as error:
            print(f"Unexpected error for {post_id}: {error}")

    return results

Replace the print statements with proper logging in production. Save failed post IDs so the script can retry them later. Do not let one broken post stop the entire update job.

Include Insights Metrics When You Have Access

Basic engagement is useful, but many teams also need reach and impressions. These usually come from the insights endpoint.

def get_post_insights(post_id):
    url = f"{BASE_URL}/{post_id}/insights"
    params = {
        "access_token": ACCESS_TOKEN,
        "metric": "post_impressions,post_impressions_unique"
    }

    response = requests.get(url, params=params, timeout=30)
    response.raise_for_status()

    metrics = {}
    for item in response.json().get("data", []):
        values = item.get("values", [])
        metrics[item.get("name")] = values[0].get("value", 0) if values else 0

    return metrics

These figures help calculate engagement rate. For example:

engagement_rate = engagement_total / impressions * 100

Only calculate the rate when impressions are greater than zero. Store the raw impression value too. If Meta later changes a metric or your finance team questions a report, raw data saves time.

Schedule the Update

Most post engagement changes quickly in the first few days, then slows down. A practical schedule is:

  • Every 2 hours for posts published in the last 24 hours.
  • Daily for posts between 2 and 14 days old.
  • Weekly for older evergreen posts.

This reduces API calls while keeping fresh data where it matters. Expect to waste time on rate limit issues if you refresh every post every hour. It is rarely needed.

You can schedule the script with cron, GitHub Actions, Airflow, Cloud Run, AWS Lambda, or a simple server task. For business reporting, use a platform that stores logs and sends alerts when a job fails.

Handle Errors and API Limits

Facebook API responses should never be treated as guaranteed. Add checks for expired tokens, missing permissions, deleted posts, restricted content, and temporary API failures.

  • Use timeouts on every request.
  • Retry temporary failures, especially 500 level errors.
  • Back off when rate limit messages appear.
  • Log response bodies for failed requests, but never log full tokens.
  • Validate fields before writing to your database.

A serious reporting pipeline should also store the date of each refresh. Engagement is not fixed. If someone asks why last Monday’s report showed 4,800 engagements and Friday’s report shows 5,260, you need a clear answer.

Best Practices for Reliable Reporting

Use one source of truth for post IDs. Keep token management separate from analytics code. Document which API version you use. Test the script against a small set of posts before running it across years of content.

Also, decide what “engagement” means before building dashboards. Some teams include clicks. Others only count reactions, comments, and shares. Both can be valid, but mixing definitions creates bad reports.

A solid Python workflow for updating Facebook engagement data has three parts: collect post IDs, refresh metrics through the Graph API, and write clean updates to storage. Once that is in place, your reports stop depending on manual exports and start reflecting what is actually happening on published posts.