Integrating Instagram Graph API into a Serverless Guest Photo Gallery: Architecture & Implementation
What Was Done
We integrated Meta's Instagram Graph API into the Ship Captain Crew guest photo gallery system to display curated @sailjada Instagram posts alongside user-uploaded charter photos. The system runs on AWS Lambda and serves the guest gallery at shipcaptaincrew.queenofsandiego.com/g/{event_id} (e.g., /g/2026-04-29). The integration required careful product configuration in the Meta App Dashboard, token lifecycle management, and Lambda environment variable setup.
Technical Details: Instagram Graph API Setup
Product Configuration in Meta App Dashboard
The critical first step was selecting the correct Meta product. The app sailjada-social (App ID: 1688884572116630) already had Messaging configured, but that use case grants different OAuth scopes than what we needed. We added a second product:
- Navigate to Add Product in the app dashboard
- Select Instagram Graph API (not Basic Display, not Messaging)
- This product grants access to
instagram_basicandpages_show_listscopes, required for reading media metadata
Why this matters: The Messaging product (used for Direct Messages) has a different scope set and doesn't include read permissions for media. Attempting to request instagram_basic scope via Messaging auth flows would fail silently—the token would be generated but lack the necessary permissions.
Account Linking & Token Generation
Once Instagram Graph API was added:
- Inside the Instagram Graph API section, access API Setup with Instagram Login
- Click Add Instagram Account and authenticate as @sailjada (the business account)
- Use the Graph API Explorer to generate a short-lived access token (valid ~2 hours)
- Select the Facebook Page linked to @sailjada's business account
- Explicitly request scopes:
instagram_basic,pages_show_list
Retrieving IG_USER_ID
With the short-lived token, we made two sequential API calls to identify the Instagram Business Account ID:
# Step 1: Get the Page ID and its linked Instagram Business Account
curl -s "https://graph.instagram.com/me?fields=id,instagram_business_account&access_token=SHORT_LIVED_TOKEN" \
| jq '.instagram_business_account.id'
# The returned "id" value is IG_USER_ID
This ID is stable and never changes for the account; it's the unique identifier needed for all subsequent media queries.
Long-Lived Token Exchange
Short-lived tokens expire in ~2 hours. For unattended Lambda execution, we exchanged it for a long-lived token (60-day validity):
curl -s "https://graph.instagram.com/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'
The returned access_token is IG_ACCESS_TOKEN, which we stored as a Lambda environment variable.
Lambda Function Integration
File: /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py
The Lambda handler in shipcaptaincrew (us-east-1, account 782785212866) now checks for these environment variables on startup:
IG_USER_ID— the Instagram Business Account IDIG_ACCESS_TOKEN— the long-lived access token
If either is missing, the Instagram integration returns an empty array. When both are present, the handler:
- Parses the event date from the URL path (e.g.,
2026-04-29) - Queries the Instagram Graph API for media posted by @sailjada within a 24-hour window
- Filters by caption keywords or hashtags (configurable per deployment)
- Returns media metadata (image URLs, captions, timestamps) to the front-end
Why Environment Variables?
Storing credentials in environment variables (rather than hardcoding) follows the 12-factor app methodology. This allows the same Lambda code to be deployed across environments (staging/production) with different credentials injected at runtime. AWS Systems Manager Parameter Store could provide additional encryption, but environment variables are sufficient for this use case given Lambda's isolated execution context.
Infrastructure & Deployment
Lambda Configuration
Environment variables were set via the AWS CLI:
aws lambda update-function-configuration \
--region us-east-1 \
--function-name shipcaptaincrew \
--environment Variables="{IG_USER_ID=YOUR_IG_USER_ID,IG_ACCESS_TOKEN=YOUR_LONG_LIVED_TOKEN}"
CloudWatch Logs (log group: /aws/lambda/shipcaptaincrew) show all token validation and API calls, crucial for debugging token expiration or scope issues.
Front-End Integration
File: /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html
The guest page (served from S3 via CloudFront) makes an AJAX call to the Lambda API endpoint:
GET /g/2026-04-29
The response includes two arrays:
- Guest uploads: approved photos from the Ship Captain Crew database, filtered by event_id
- Instagram posts: @sailjada media from the same date, merged client-side by timestamp
The HTML/JavaScript displays these in a gallery layout, with attribution to the original poster (guest name or @sailjada handle).
Key Decisions
60-Day Token Lifecycle vs. OAuth Flow Refresh
We chose long-lived tokens (60-day expiration) with manual refresh rather than storing the user's password or implementing a full OAuth refresh token flow. Reasoning:
- Simpler operation: a single environment variable, no refresh logic in Lambda
- More secure: we never store @sailjada's password; only the delegated token is at risk
- Acceptable trade-off: 60 days between manual refreshes is reasonable for a low-frequency gallery feature
If higher availability is needed, this could be upgraded to EventBridge-triggered Lambda that automatically exchanges the old token for a new one before expiration.