Integrating Instagram Graph API with AWS Lambda: Connecting @sailjada Media to Guest Photo Pages
What Was Done
We enabled Instagram media integration for the shipcaptaincrew Lambda function (us-east-1, account 782785212866) to dynamically surface @sailjada Instagram posts alongside guest-uploaded charter photos on the guest photo gallery pages at shipcaptaincrew.queenofsandiego.com/g/{event_id}. The Lambda function previously contained dormant Instagram integration code that returned empty arrays when credentials were missing. This work activates that integration by establishing proper OAuth token flow through Facebook's Instagram Graph API.
Technical Details: Instagram Graph API Authentication Flow
Instagram's Graph API requires a multi-step token acquisition process that differs significantly from standard OAuth2 flows. Understanding this sequence is critical for maintenance and token refresh.
Step 1: Register the Correct Product
The initial blocker: the existing sailjada-social Facebook app had the Instagram Messaging product configured, which grants the instagram_manage_messages scope. This is insufficient for reading media. The solution required adding a second product:
- Navigate to
developers.facebook.com/apps - Select the
sailjada-socialapp - Click Add Product in the left sidebar
- Search for and select Instagram Graph API (distinct from Basic Display and Messaging)
Why this matters: Product selection in Facebook's developer ecosystem determines which OAuth scopes are available. Graph API is the only product that exposes instagram_basic scope, which allows reading media metadata, captions, and timestamps.
Step 2: Link the Business Account
Inside Instagram Graph API settings:
- Navigate to API setup with Instagram login
- Click Add Instagram account
- Authenticate as @sailjada (the account must be a Business or Creator account, not Personal)
This creates a linkage between the app registration and the actual Instagram account, enabling the account holder to grant permissions through OAuth.
Step 3: Generate Short-Lived Access Token
Using Facebook's Graph Explorer as the trusted testing environment:
- Open
developers.facebook.com/tools/explorer - Select
sailjada-socialfrom the app dropdown - Generate Access Token
- When prompted, select the Facebook Page linked to @sailjada
- Explicitly request scopes:
instagram_basic,pages_show_list
This produces a short-lived token (valid ~2 hours) for immediate testing and credential extraction.
Step 4: Extract IG_USER_ID
With the short-lived token, make two sequential API calls to obtain the Instagram business account ID:
curl -X GET "https://graph.instagram.com/v18.0/me/accounts?access_token=SHORT_LIVED_TOKEN"
This returns a list of Facebook Pages managed by the authenticated user. Identify the page linked to @sailjada and note its id field. Then:
curl -X GET "https://graph.instagram.com/v18.0/{PAGE_ID}?fields=instagram_business_account&access_token=SHORT_LIVED_TOKEN"
The response contains instagram_business_account.id — this is your IG_USER_ID. This ID remains constant and is safe to store in environment variables.
Step 5: Exchange for Long-Lived Token
Short-lived tokens expire within hours. Obtain a token valid for 60 days:
curl -X GET "https://graph.instagram.com/v18.0/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"
The response access_token field is your IG_ACCESS_TOKEN, valid for 60 days and suitable for Lambda environment variables.
Infrastructure: Lambda Configuration
Once credentials are obtained, update the Lambda function with environment variables:
aws lambda update-function-configuration \
--function-name shipcaptaincrew \
--region us-east-1 \
--environment Variables='{IG_USER_ID=,IG_ACCESS_TOKEN=}'
The Lambda function (runtime Python 3.x, timeout 30s) contains conditional logic in its guest photo response handler: if both IG_USER_ID and IG_ACCESS_TOKEN environment variables are present and non-empty, it calls the Instagram Graph API endpoint:
https://graph.instagram.com/v18.0/{IG_USER_ID}/media?fields=id,caption,timestamp,media_type,media_url&access_token={IG_ACCESS_TOKEN}
Results are filtered by timestamp to match the event date window and merged with guest-uploaded photos in the response JSON. If credentials are absent, the function gracefully returns an empty Instagram array, maintaining backward compatibility.
Key Architectural Decisions
Why Graph API over Basic Display?
Instagram's Basic Display API is simpler but offers only media URLs and captions—no timestamps. For the guest photo feature to be useful, we need to filter Instagram posts by event date. Graph API provides timestamp, enabling intelligent temporal filtering.
Why 60-Day Tokens?
Instagram's token lifecycle is intentionally short to force regular credential rotation, reducing risk from token leakage. A 60-day window provides a practical maintenance cadence without requiring weekly refreshes. However, this means calendar reminders or automated refresh are necessary.
Why Environment Variables Instead of Secrets Manager?
For this use case, Lambda environment variables suffice because the Instagram token is read-only (no write permissions) and scoped only to @sailjada's public media. Secrets Manager would add latency and cost without proportional security gain. If token management scaled to dozens of Instagram accounts, migration to Secrets Manager would be warranted.
Testing & Verification
After Lambda deployment, verify the integration by accessing a gallery page with a recent event date:
https://shipcaptaincrew.queenofsandiego.com/g/2026-04-29
Check the page source or network inspector to confirm Instagram posts appear in the response JSON alongside guest photos. If the array remains empty, verify:
- IG_USER_ID and IG_ACCESS_TOKEN are set in Lambda environment
- @sailjada has Instagram posts from the queried date
- Token has not expired (60-day window from exchange date)
- Lambda CloudWatch logs for API errors
What's Next: Token Refresh Strategy
Before the 60-day window closes, tokens must be refreshed using the same exchange flow. Two options: