Integrating Instagram Graph API with AWS Lambda: Connecting @sailjada Social Media to Guest Photo Events
What Was Done
We integrated Instagram's Graph API into the shipcaptaincrew Lambda function (us-east-1, account 782785212866) to surface @sailjada Instagram posts alongside guest-uploaded charter photos on the guest photo page at shipcaptaincrew.queenofsandiego.com/g/{event_id}. The Lambda was already structured to accept Instagram data via environment variables (IG_USER_ID and IG_ACCESS_TOKEN), but those credentials were missing and the integration was dormant. This walkthrough covers the complete path from Facebook app configuration through Lambda deployment.
Why This Architecture
Rather than polling Instagram at request time, we opted to store credentials in Lambda environment variables and fetch media within the function. This approach:
- Reduces latency: No external API round-trip during page render; media fetch happens server-side in Lambda and is bundled with guest photo responses.
- Simplifies caching: We can layer CloudFront or Lambda response caching without managing Instagram rate limits on the client.
- Keeps secrets centralized: Credentials live in AWS Secrets Manager and are injected as environment variables at function initialization, not hardcoded.
- Decouples Instagram from frontend: The guest photo page receives a unified JSON response with both guest and Instagram media, no cross-origin calls needed.
Technical Details: The Instagram Graph API Setup
Step 1: Add the Correct Product to Your Facebook App
The initial blocker was that the sailjada-social app had the Instagram Messaging product added (for DM functionality), but this product doesn't grant the instagram_basic scope required to read media. We needed to add the Instagram Graph API product separately:
- Navigate to
developers.facebook.com/apps - Open the
sailjada-socialapp - In the left sidebar, click Add Product
- Search for and select Instagram Graph API (distinct from Instagram Basic Display)
- Complete the setup flow; Instagram Graph API now appears in your product sidebar
Why this matters: Each product in Facebook's developer ecosystem grants different permissions. Messaging is for user-to-user communication; Graph API is for accessing public account data (posts, media, insights). Attempting to use Messaging scopes for media reads will silently fail or return empty arrays.
Step 2: Confirm Account Setup and Generate Tokens
The @sailjada Instagram account must be a Business or Creator account and linked to a Facebook Page. Once verified:
- In Instagram Graph API settings, click API setup with Instagram login
- Click Add Instagram account and authenticate as @sailjada
- Navigate to
developers.facebook.com/tools/explorer - Select app
sailjada-socialfrom the dropdown - Click Generate Access Token
- In the permissions dialog, select the Facebook Page linked to @sailjada
- Request scopes:
instagram_basic,pages_show_list
This generates a short-lived token (valid ~2 hours) used only to bootstrap the process.
Step 3: Retrieve IG_USER_ID via Graph API Calls
With the short-lived token, make two API calls to locate the Instagram Business Account ID:
curl -s "https://graph.instagram.com/me?fields=id,name&access_token=SHORT_LIVED_TOKEN"
# Returns your Facebook Page ID; note it
curl -s "https://graph.instagram.com/{PAGE_ID}?fields=instagram_business_account&access_token=SHORT_LIVED_TOKEN"
# Returns: {"instagram_business_account":{"id":"XXXXXXXXX"}}
# That ID value = IG_USER_ID
Why two calls: The Graph API enforces role-based access. Your app's token is tied to your Page, not directly to the Instagram account. We must traverse: Token → Page → Instagram Business Account.
Step 4: Exchange for Long-Lived Token
The short-lived token expires in hours. Exchange it for a long-lived token (60 days) using your app credentials:
curl -s "https://graph.instagram.com/oauth/access_token" \
-d "grant_type=fb_exchange_token" \
-d "client_id=YOUR_APP_ID" \
-d "client_secret=YOUR_APP_SECRET" \
-d "access_token=SHORT_LIVED_TOKEN"
# Returns: {"access_token":"LONG_LIVED_TOKEN","token_type":"bearer"}
# That access_token value = IG_ACCESS_TOKEN
Store both IG_USER_ID and IG_ACCESS_TOKEN securely.
Infrastructure: Lambda Configuration
Update the shipcaptaincrew Lambda function with the credentials:
aws lambda update-function-configuration \
--region us-east-1 \
--function-name shipcaptaincrew \
--environment Variables={IG_USER_ID=XXXXXXXXX,IG_ACCESS_TOKEN=LONG_LIVED_TOKEN_VALUE}
The Lambda code in the function already checks for these environment variables:
ig_user_id = os.environ.get('IG_USER_ID')
ig_access_token = os.environ.get('IG_ACCESS_TOKEN')
if ig_user_id and ig_access_token:
# Fetch recent media from IG
ig_media = fetch_instagram_media(ig_user_id, ig_access_token)
else:
ig_media = []
Once environment variables are set, the function's dormant Instagram integration activates and returns media for the guest photo page.
Token Refresh Strategy
Long-lived tokens expire after 60 days. Rather than manual rotation, implement automated refresh via EventBridge:
- Create an EventBridge rule named
instagram-token-refresh - Schedule:
cron(0 0 1 * ? *)(first of each month at UTC midnight) - Target: Lambda function that calls the exchange endpoint and updates
shipcaptaincrewenvironment variables - This refresh function uses your app credentials (stored in AWS Secrets Manager) to rotate the token automatically
Why EventBridge over cron: EventBridge integrates natively with Lambda and CloudWatch Logs for monitoring. It's AWS-native and doesn't require external tooling or EC2 instances.
Key Decisions and Rationale
- Environment variables over Secrets Manager: For non-sensitive tokens (Instagram's long-lived tokens are rotated frequently and not equivalent to passwords), environment variables provide faster access and simpler Lambda configuration. Secrets Manager would add latency and complexity without meaningful security gain in this case.