Integrating Instagram Graph API with AWS Lambda: Connecting @sailjada Social Media to Guest Photo Pages

What Was Done

We integrated the Instagram Graph API into the shipcaptaincrew Lambda function to surface @sailjada Instagram posts alongside user-uploaded charter photos on guest event pages (e.g., shipcaptaincrew.queenofsandiego.com/g/2026-04-29). The integration was previously dormant, returning an empty array when environment variables were missing. This post covers the complete setup: adding the correct Facebook/Instagram product, obtaining long-lived API tokens, storing them securely in Lambda environment variables, and verifying the integration end-to-end.

Technical Details: Instagram Graph API Setup

Step 1: Adding the Instagram Graph API Product (The Critical First Step)

The most common blocker in this flow is adding the wrong product to your Facebook app. The Facebook app dashboard offers multiple Instagram-related products:

  • Instagram Basic Display — read-only access to basic profile info and media captions (no reach/impressions)
  • Instagram Graph API — full read access to media, insights, stories, and comments
  • Messaging — handles Instagram DMs only; does not grant media read permissions

If you previously added "Messaging" expecting to read media, you'll need to add Instagram Graph API as a separate product. The Messaging use case is specifically for direct message handling and will not grant the instagram_basic scope required to fetch posts.

Action:

  • Navigate to developers.facebook.com/apps
  • Select the app: sailjada-social (account 782785212866, us-east-1)
  • In the left sidebar, click Add Product
  • Search for and select Instagram Graph API (not Basic Display)
  • Complete the setup flow — the product will appear in your sidebar

Step 2: Verify @sailjada Account Status

The account being integrated must be a Business or Creator account, not a personal account. Additionally, @sailjada must be connected to a Facebook Page that your app can access.

  • Confirm @sailjada is set to Business or Creator account status (Settings → Account Type)
  • Verify the account is linked to a Facebook Page (Settings → Linked Accounts)

Step 3: Generate a Short-Lived Access Token

The Graph API Explorer is the fastest way to generate an initial short-lived token with the correct scopes. This token is valid for about 1 hour and is used only to obtain a long-lived token.

  • Go to developers.facebook.com/tools/explorer
  • Select app: sailjada-social
  • Click Generate Access Token
  • Select the Facebook Page linked to @sailjada
  • In the permissions dialog, ensure these scopes are checked:
    • instagram_basic — read media and profile info
    • pages_show_list — list pages your app manages
  • Copy the generated token (valid ~1 hour)

Step 4: Retrieve the Instagram Business Account ID (IG_USER_ID)

The Instagram Business Account ID is the unique identifier for @sailjada in the Graph API. You'll need this to make subsequent media queries.

# Get all pages managed by your app
curl -s "https://graph.instagram.com/me/accounts?access_token=YOUR_SHORT_LIVED_TOKEN" | jq '.'

# Response will include your Facebook Page ID. Then:
curl -s "https://graph.instagram.com/FACEBOOK_PAGE_ID?fields=instagram_business_account&access_token=YOUR_SHORT_LIVED_TOKEN" | jq '.'

# The "id" field inside instagram_business_account is your IG_USER_ID

Save the IG_USER_ID value — you'll need it for Lambda configuration and for making media requests.

Step 5: Exchange for a Long-Lived Token (60-Day Validity)

Short-lived tokens are intended for one-time use. To make ongoing API calls without re-authenticating every hour, exchange the short-lived token for a long-lived token valid for approximately 60 days.

curl -s "https://graph.instagram.com/oauth/access_token?grant_type=fb_exchange_token&client_id=YOUR_APP_ID&client_secret=YOUR_APP_SECRET&access_token=YOUR_SHORT_LIVED_TOKEN" | jq '.'

# Response includes "access_token" (valid ~60 days) and "expires_in" (seconds)

Why exchange tokens: Storing a 1-hour token in Lambda environment variables is impractical. A 60-day token reduces the need for frequent re-authentication while still maintaining reasonable security boundaries. (See "Token Refresh Strategy" below.)

Infrastructure: Lambda Environment Variable Configuration

Once you have both the IG_USER_ID and the 60-day access token, store them as environment variables in the shipcaptaincrew Lambda function.

aws lambda update-function-configuration \
  --function-name shipcaptaincrew \
  --region us-east-1 \
  --environment Variables="{IG_USER_ID=YOUR_IG_USER_ID,IG_ACCESS_TOKEN=YOUR_LONG_LIVED_TOKEN}" \
  --profile default

Why environment variables: Lambda environment variables are encrypted at rest (using the AWS Lambda default KMS key or a custom key). They're the standard pattern for non-sensitive configuration that Lambda code reads at runtime. Since these values are specific to the @sailjada account and the integration is read-only (no write permissions), the security posture is acceptable.

Location in the Lambda console: Function → Configuration → Environment variables

How the Lambda Function Uses These Variables

The shipcaptaincrew function (Python 3.x, located in us-east-1) checks for these environment variables at runtime. When both are present, it makes a request to the Instagram Graph API to fetch recent posts from @sailjada within a configurable time window (matching the guest photo event date).

Pseudocode (actual implementation in the function):

import boto3
import os
import requests

IG_USER_ID = os.environ.get("IG_USER_ID")
IG_ACCESS_TOKEN = os.environ.get("IG_ACCESS_TOKEN")

def fetch_instagram_posts(event_date):
    if not IG_USER_ID or not IG_ACCESS_TOKEN:
        return []  # Return empty array if not configured
    
    # Fetch media from @sailjada filtered by event_date window
    url = f"https://graph.instagram.com/{IG_USER_ID}/media"
    params = {
        "fields": "id,caption,media_type,media_url,timestamp,like_count",
        "access_token": IG_ACCESS_TOKEN
    }
    response = requests.get(url, params=params)
    return response.json().get("