```html

Integrating Instagram Graph API with Lambda: Building a Guest Photo Gallery with Social Media Aggregation

What Was Done

We enabled Instagram Graph API integration for the shipcaptaincrew Lambda function to aggregate @sailjada Instagram posts alongside guest-uploaded charter photos in the guest gallery system. The gallery lives at shipcaptaincrew.queenofsandiego.com/g/{event_id} (e.g., /g/2026-04-29) and now dynamically pulls Instagram media from the same day/time window as charter events.

The integration required three infrastructure changes: (1) adding the Instagram Graph API product to the Facebook app, (2) generating and exchanging access tokens for long-lived credentials, and (3) updating Lambda environment variables to enable the dormant IG integration code.

Technical Details: The Architecture

File Structure and Code Organization

The Lambda function source code lives at:

/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py

The front-end gallery view is in:

/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/index.html

The Lambda handler already contained stub code for Instagram integration that returned an empty array when environment variables IG_USER_ID and IG_ACCESS_TOKEN were missing. The logic was sound—it needed only credentials to activate.

Why Instagram Graph API Instead of Basic Display?

Instagram offers multiple API products for different use cases. We chose Instagram Graph API because it grants the instagram_basic scope, which allows reading media metadata (captions, timestamps, media URLs). The Messaging product (which we initially added) only handles DMs and would not authorize media reads. Basic Display requires manual user approval for each follower account, making it unsuitable for automated aggregation.

The Token Exchange Flow

The integration follows Facebook's token lifecycle pattern:

  • Short-lived token (2 hours): Generated via Graph API Explorer with manual scopes selection, used to query the Facebook Page linked to @sailjada
  • IG_USER_ID extraction: Query the Page's instagram_business_account field to retrieve the Instagram Business Account ID
  • Long-lived token exchange: Exchange the short-lived token for a long-lived token (60-day validity) using app credentials (APP_ID + APP_SECRET)
  • Monthly refresh strategy: The exchange call can be repeated monthly before expiration; optionally automated via EventBridge scheduled rules

Lambda Integration Points

The handler function in lambda_function.py contains a private method that, when IG_USER_ID and IG_ACCESS_TOKEN are set, constructs a query to the Instagram Graph API endpoint:

GET https://graph.instagram.com/{IG_USER_ID}/media
  ?fields=id,caption,media_type,media_url,timestamp
  &access_token={IG_ACCESS_TOKEN}

The response is filtered by event date/time window and merged with guest photo records before rendering in the front-end index.html template.

Infrastructure Changes

Lambda Configuration Updates

The Lambda function shipcaptaincrew in region us-east-1 (AWS Account 782785212866) was updated with new environment variables via the AWS CLI:

aws lambda update-function-configuration \
  --function-name shipcaptaincrew \
  --region us-east-1 \
  --environment Variables={IG_USER_ID=<value>,IG_ACCESS_TOKEN=<value>}

No code changes were deployed; the handler already contained the conditional logic. Environment variables are the deployment boundary here.

Monitoring and Debugging

CloudWatch Logs for the function live at:

/aws/lambda/shipcaptaincrew

During development, we tailed logs to verify token expiration errors and API quota issues:

aws logs tail /aws/lambda/shipcaptaincrew --region us-east-1 --follow

S3 and CloudFront Distribution

The index.html file is deployed to the S3 bucket backing the CloudFront distribution for shipcaptaincrew.queenofsandiego.com. No cache invalidation was required since the gallery fetches dynamic data from the Lambda API endpoint, not static assets.

Key Decisions and Rationale

Why Not Store Tokens in Parameter Store?

We chose environment variables over AWS Systems Manager Parameter Store for simplicity: tokens have short lifecycles (60 days for long-lived Instagram tokens), and Parameter Store adds latency and complexity. For truly sensitive values, Parameter Store with encryption would be preferred, but for temporary API credentials with built-in expiration, environment variables are acceptable and faster.

Why Monthly Refresh Instead of Automatic EventBridge Refresh?

We implemented a manual monthly refresh strategy initially to avoid operational overhead. Automatic refresh via EventBridge (triggering a Lambda that exchanges tokens and updates function configuration) is a future optimization. Manual refresh keeps the system simple and avoids unnecessary IAM permissions or Lambda-to-Lambda calls.

Filtering by Event Date/Time Window

The Lambda handler filters Instagram posts by event timestamp (±2 hours). This prevents showing unrelated posts and keeps the gallery focused on the charter event. The logic is in the handler, not in the Graph API query itself, because Instagram's filtering capabilities are limited—we fetch all media and filter client-side.

Testing and Verification

After updating environment variables, we verified the integration by directly requesting a gallery page:

curl -s "https://shipcaptaincrew.queenofsandiego.com/g/2026-04-29"

The response includes both guest-uploaded photos (from DynamoDB) and aggregated Instagram posts (from Graph API) in the same JSON payload, rendered by the front-end template.

CloudWatch Logs confirmed successful Graph API calls and proper filtering without quota errors.

What's Next

  • EventBridge automation: Create a scheduled rule to invoke a token-refresh Lambda 55 days after each token exchange, preventing manual maintenance
  • Token storage upgrade: Migrate to AWS Secrets Manager for credentials if we expand to multiple Instagram accounts or add other OAuth integrations
  • Webhook integration: Implement Instagram webhook subscriptions to push new posts in real-time rather than polling on gallery load
  • Caching layer: Add ElastiCache to avoid repeated Graph API calls for the same event within a short window, reducing latency and API quota usage
```