Integrating Instagram Graph API with Lambda for Guest Photo Timeline Aggregation
What Was Done
We implemented Instagram Graph API integration into a Lambda-based guest photo gallery system to display @sailjada Instagram posts alongside user-uploaded charter photos, filtered by event date and time window. The integration required OAuth token exchange, environment variable configuration in Lambda, and careful API scope management to ensure the function could read Instagram media while maintaining security boundaries.
Architecture Overview
The guest photo system is deployed at shipcaptaincrew.queenofsandiego.com/g/{event_id} (e.g., /g/2026-04-29) and consists of:
- Static frontend: HTML/JS served from S3 bucket
queenofsandiego.com-shipcaptaincrew-assetsvia CloudFront - Lambda function:
shipcaptaincrewinus-east-1(account782785212866) handling API endpoints - Guest uploads: Stored in DynamoDB with approval workflow; CloudFront caches approved images
- Instagram integration: Graph API calls from Lambda to fetch media filtered by timestamp
Previously, the Lambda contained dormant Instagram code that returned empty arrays when environment variables were missing. This update activates that capability by establishing proper OAuth tokens and configuring the runtime environment.
Technical Implementation Details
Step 1: Adding Instagram Graph API Product
The critical first step was adding the correct product to the Facebook App. Navigate to developers.facebook.com/apps → sailjada-social → Add Product and select Instagram Graph API (not Basic Display, not Messaging). The Messaging product, which may have been added earlier, does not grant the instagram_basic scope needed for media reads.
Why this distinction matters: Facebook's product architecture separates concerns. Messaging handles DM APIs; Graph API handles content reads. Scope availability depends on the product selection, not just the app configuration.
Step 2: Account Connection and Short-Lived Token Generation
Once Instagram Graph API is added, the next step is connecting the @sailjada account:
- Instagram Graph API → API setup with Instagram login → Add Instagram account
- Log in as @sailjada (the account must be a Business or Creator account linked to a Facebook Page)
- Navigate to Graph API Explorer, select
sailjada-socialapp, and generate an access token - When prompted for scopes, select:
instagram_basic,pages_show_list
This token is short-lived (approximately 1 hour) and used only to bootstrap the long-lived token exchange.
Step 3: Retrieving IG_USER_ID from Facebook Page
The Instagram Graph API requires the numeric Instagram Business Account ID, not the username. Retrieve it by querying the Facebook Page linked to @sailjada:
# First call: Get the Page ID and Instagram Business Account reference
curl -s "https://graph.facebook.com/v18.0/me/accounts?access_token=SHORT_LIVED_TOKEN" \
| jq '.data[] | select(.name=="Your Page Name") | .id'
# Store the page_id, then call:
curl -s "https://graph.facebook.com/v18.0/{PAGE_ID}?fields=instagram_business_account&access_token=SHORT_LIVED_TOKEN" \
| jq '.instagram_business_account.id'
The returned id value is your IG_USER_ID. This is immutable and won't change unless the Page-Account link is severed.
Step 4: Long-Lived Token Exchange
Facebook's access tokens expire. The short-lived token (1 hour) must be exchanged for a long-lived token (60 days) suitable for Lambda environment variables:
curl -s "https://graph.facebook.com/v18.0/oauth/access_token?grant_type=fb_exchange_token&client_id=APP_ID&client_secret=APP_SECRET&fb_exchange_token=SHORT_LIVED_TOKEN"
The response includes a new access_token (good for 60 days) that becomes your IG_ACCESS_TOKEN. Store this securely in AWS Secrets Manager or Parameter Store, not as plaintext in code.
Infrastructure and Environment Configuration
Update the Lambda function configuration with the two values retrieved above:
aws lambda update-function-configuration \
--function-name shipcaptaincrew \
--region us-east-1 \
--environment Variables="{IG_USER_ID=your_id_here,IG_ACCESS_TOKEN=your_token_here}"
The Lambda runtime environment is defined in /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py. The handler checks for these variables at startup; if present, Instagram queries are enabled. The function constructs a time-window filter to match photos from the event date:
- Event date parsing: From the URL path
/g/2026-04-29, extract the date - Time window: Typically ±12 hours around the event time (configurable)
- API call: Query Instagram Business Account for media posted within that window
- Merge logic: Combine Instagram posts with approved DynamoDB uploads in the response JSON
Key Decisions and Rationale
Why long-lived tokens instead of refresh-token flow? Long-lived tokens (60-day expiration) simplify Lambda stateless design. There's no need to store refresh tokens or implement token refresh logic within the function. A monthly or bi-monthly manual refresh (via a CI/CD pipeline or scheduled Lambda) is simpler and more reliable than automatic refresh code.
Why store tokens in environment variables rather than code? Environment variables keep secrets out of version control. For production deployments, use AWS Secrets Manager with Lambda IAM policies instead of plaintext environment variables. Update the execution role:
aws iam put-role-policy \
--role-name shipcaptaincrew-execution-role \
--policy-name instagram-secrets-access \
--policy-document file://policy.json
Why filter by time window? Instagram accounts may have hundreds of posts. Time-window filtering reduces API calls and latency, returning only relevant media from the event day.
Verification and Testing
After Lambda configuration, test the integration:
curl -si "https://shipcaptaincrew.queenofsandiego.com/g/2026-04-29"
Check the response for an instagram_posts field in the JSON response. If Instagram posts are absent or empty, verify:
- Environment variables are set:
aws lambda get-function-configuration --function-name shipcaptaincrew - Token hasn't expired (60-day window)