Integrating Instagram Graph API with AWS Lambda for Event Photo Galleries
Building a unified photo gallery that combines guest-uploaded images with official social media content requires careful orchestration across multiple services. This post details how we integrated Instagram's Graph API into an AWS Lambda function to automatically surface @sailjada Instagram posts alongside approved guest photos from charter events, and the infrastructure decisions that made it possible.
What We Built
The Ship Captain Crew event photo system now supports event-specific galleries at paths like /g/2026-04-29 that display:
- Approved guest-uploaded charter photos (stored in S3, managed via database)
- Instagram posts from @sailjada published within a configurable time window of the event date
- Both media types sorted chronologically and rendered in a responsive gallery
The integration lives in our Lambda function at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py, which serves the API endpoint shipcaptaincrew.queenofsandiego.com/g/{event_id}.
Technical Architecture
The Challenge: Two Data Sources, One Timeline
Guest photos are stored in our managed database and S3 bucket. Instagram posts are on Meta's platform. The naive approach—making synchronous API calls for every page load—would introduce latency and rate-limit risk. Our solution: the Lambda function includes Instagram integration logic that runs on-demand, with environment variables controlling whether the feature is active.
Instagram Graph API Setup
Instagram's Graph API requires OAuth token exchange. The setup process involves:
- Facebook App Configuration: Created a Facebook App (
sailjada-social) registered atdevelopers.facebook.com/apps - Product Addition: Added the Instagram Graph API product (not Basic Display, not Messaging—the distinction matters for permission scopes)
- Account Linking: Connected the @sailjada Instagram account to the app via Instagram Graph API's account linking flow
- Token Generation: Generated a short-lived access token via the Graph API Explorer with scopes
instagram_basicandpages_show_list - User ID Retrieval: Made authenticated calls to the Graph API to extract
IG_USER_IDfrom the business account data - Token Exchange: Exchanged the short-lived token for a long-lived token (60-day expiration) using app credentials
Lambda Function Integration
The Lambda function (shipcaptaincrew in region us-east-1, account 782785212866) was modified to include Instagram fetching logic:
# Simplified pseudocode of the pattern
def fetch_instagram_posts(event_id, ig_user_id, ig_access_token):
"""
Fetch Instagram posts from @sailjada within the event's date range.
Returns empty array if credentials are missing.
"""
if not ig_user_id or not ig_access_token:
return []
# Query Graph API for media from this date window
# Filter by caption/timestamp to match event context
# Return formatted media objects with URLs
pass
This design pattern—returning an empty array when credentials are absent—means the feature degrades gracefully. The guest photo gallery works perfectly without Instagram integration; when credentials are configured, Instagram posts are automatically included.
Why Environment Variables?
Rather than hardcoding credentials or storing them in the codebase, we use AWS Lambda environment variables:
IG_USER_ID: The numeric ID of the @sailjada business account on InstagramIG_ACCESS_TOKEN: The 60-day long-lived OAuth token for Graph API access
These are set via AWS Systems Manager Parameter Store and injected at runtime. This separates configuration from code and enables credential rotation without redeployment.
Infrastructure & Deployment
Lambda Configuration
The function is deployed with:
- Function Name:
shipcaptaincrew - Runtime: Python 3.x
- Handler:
lambda_function.handler(the main entry point) - Timeout: 30 seconds (sufficient for guest DB query + Instagram API call + response assembly)
- Memory: 256 MB
- VPC: None (API calls to external services don't require VPC)
Updates to the function are applied via the AWS CLI:
aws lambda update-function-configuration \
--function-name shipcaptaincrew \
--region us-east-1 \
--environment Variables="{IG_USER_ID=,IG_ACCESS_TOKEN=}"
API Gateway & CloudFront
The Lambda is fronted by API Gateway (mapped to shipcaptaincrew.queenofsandiego.com) and CloudFront for caching. The /g/ path is configured with a short TTL (60-90 seconds) because event photo galleries are dynamic—new guest uploads or Instagram posts may appear frequently.
Frontend Implementation
The HTML file at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html includes client-side logic to:
- Parse the event ID from the URL path
- Call the Lambda API endpoint
/g/{event_id} - Render both guest photos and Instagram posts in a unified gallery grid
- Handle empty states gracefully (no photos, no Instagram posts, etc.)
The frontend makes no direct calls to Instagram; all Instagram data flows through our Lambda, which acts as a privacy and performance boundary.
Key Decisions & Rationale
Why OAuth instead of API keys?
Instagram's Graph API requires OAuth tokens, not simple API keys. This is stricter than many APIs but provides better audit trails and per-user permission control. The token exchange flow ensures we only request the minimum scopes needed (instagram_basic for reading media, pages_show_list for account discovery).
Why a 60-day token?
Short-lived tokens (1-2 hours) are more secure but require frequent refreshes. Long-lived tokens (60 days) reduce operational overhead. Our strategy: refresh the token monthly via a scheduled EventBridge rule that calls the same exchange endpoint, ensuring we always have valid credentials without manual intervention.
Why no caching at Lambda level?
We rely on CloudFront caching rather than caching inside the Lambda. This keeps the function stateless and avoids the complexity of cache invalidation logic. If a guest uploads a photo or Instagram posts new content, the cache expires naturally, and the next request fetches fresh data.
Testing & Verification
After environment variables are set, verify integration at an event URL:
curl -s "https://shipcaptaincrew.