Integrating Instagram Graph API with Lambda: Connecting Guest Charter Photos to Social Media
What Was Done
We enabled Instagram media integration for the ShipCaptainCrew guest photo gallery system. The application at shipcaptaincrew.queenofsandiego.com/g/{event_id} displays user-uploaded charter photos alongside official @sailjada Instagram posts from matching date/time windows. Previously, the Instagram integration was dormant due to missing authentication credentials. This project involved:
- Configuring the Instagram Graph API product in the Facebook App Dashboard
- Implementing OAuth token exchange flow to obtain long-lived access credentials
- Updating Lambda environment variables with Graph API credentials
- Establishing a token refresh strategy for 60-day credential rotation
Technical Details: Architecture Overview
The system uses a three-tier architecture:
- Frontend: Static HTML at
/tools/shipcaptaincrew/index.htmlserved by CloudFront - API Layer: AWS Lambda function
shipcaptaincrewin us-east-1 (account 782785212866) handling guest photo queries and Instagram media fetching - Data: Guest-approved photos stored in DynamoDB; Instagram data fetched on-demand via Graph API
When a user visits /g/2026-04-29, the frontend makes an API call to the Lambda handler. The Lambda function:
- Queries DynamoDB for approved photos matching the event_id (date)
- Calls Instagram Graph API with the stored IG_USER_ID and IG_ACCESS_TOKEN environment variables
- Filters Instagram posts by timestamp to match the charter window
- Returns a merged JSON response combining both photo sources
Infrastructure Changes: Facebook App Configuration
The core issue was that the Lambda function referenced environment variables (IG_USER_ID, IG_ACCESS_TOKEN) that were empty. The app had a "Manage messaging" product configured, which grants Direct Message permissions but not the instagram_basic scope required to read media.
The fix required adding the correct product to the Facebook App (sailjada-social, ID: 1688884572116630):
- Navigate to app dashboard → Add Product
- Select Instagram Graph API (distinct from Basic Display or Messaging APIs)
- This grants access to media endpoints and scopes needed for
/ig_hashtag_searchand/mediaqueries
The @sailjada Instagram account (a Business/Creator account) needed to be explicitly connected to the app:
- Within Instagram Graph API settings → API setup with Instagram login
- Authenticate as @sailjada to authorize the app to read its media
- This creates the linkage between the Instagram account and the Facebook Page managing the app
OAuth Token Flow: Short-lived to Long-lived Exchange
Facebook's Instagram Graph API uses a two-tier token system. The process:
# Step 1: Generate short-lived token (valid ~2 hours)
# Using Graph API Explorer at developers.facebook.com/tools/explorer
# - Select app: sailjada-social
# - Select FB Page linked to @sailjada
# - Request scopes: instagram_basic, pages_show_list
# Token appears in the explorer interface
# Step 2: Retrieve IG_USER_ID from the FB Page
curl -s "https://graph.instagram.com/v18.0/{PAGE_ID}/instagram_business_account?access_token={SHORT_LIVED_TOKEN}" \
| jq '.instagram_business_account.id'
# Returns: 17841400000000000 (example format)
# Step 3: Exchange short-lived token for long-lived token (valid ~60 days)
curl -s "https://graph.instagram.com/v18.0/oauth/access_token" \
-d "grant_type=fb_exchange_token" \
-d "client_id={APP_ID}" \
-d "client_secret={APP_SECRET}" \
-d "access_token={SHORT_LIVED_TOKEN}" \
| jq '.access_token'
# Returns: long-lived token string
Once obtained, these two values were stored as Lambda environment variables:
IG_USER_ID— the Instagram Business Account IDIG_ACCESS_TOKEN— the long-lived token (60-day validity)
The Lambda function then uses these to call the media endpoint:
# Inside lambda_function.py handler
import os
import requests
ig_user_id = os.environ.get('IG_USER_ID')
ig_token = os.environ.get('IG_ACCESS_TOKEN')
url = f"https://graph.instagram.com/v18.0/{ig_user_id}/media"
params = {
'fields': 'id,caption,media_type,media_url,timestamp',
'access_token': ig_token
}
response = requests.get(url, params=params)
media_list = response.json().get('data', [])
Updating Lambda Configuration
Environment variables were updated using the AWS CLI:
aws lambda update-function-configuration \
--function-name shipcaptaincrew \
--region us-east-1 \
--environment Variables={IG_USER_ID=17841400000000000,IG_ACCESS_TOKEN=IGABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz...}
The Lambda function file at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py already contained the logic to fetch and filter Instagram media; it was simply waiting for valid credentials.
Key Design Decisions
Why use long-lived tokens instead of refreshing via user flow? Instagram Graph API long-lived tokens remain valid for 60 days without user interaction. Since this is a backend service (not a user-facing OAuth flow), a long-lived token stored in Lambda environment variables is appropriate. If the token expires in production, it's refreshed using the same exchange endpoint with APP_ID and APP_SECRET—no user re-authentication needed.
Why filter by timestamp in Lambda rather than at query time? Instagram's Graph API doesn't accept timestamp range filters on the media endpoint. Filtering in Lambda is simpler than building a separate indexing layer and allows flexibility to adjust the time window (e.g., ±2 hours around the charter).
Why not cache Instagram data? The dataset is small (typically 5–15 posts per charter date). Fetching on-demand keeps the guest gallery fresh without adding DynamoDB writes or cache invalidation logic.
Testing and Verification
After updating environment variables, the guest gallery was tested at: