```html

Integrating Instagram Graph API with Lambda: Connecting Guest Charter Photos to Social Media

What Was Done

We enabled Instagram media integration for the ShipCaptainCrew guest photo gallery system. The application at shipcaptaincrew.queenofsandiego.com/g/{event_id} displays user-uploaded charter photos alongside official @sailjada Instagram posts from matching date/time windows. Previously, the Instagram integration was dormant due to missing authentication credentials. This project involved:

  • Configuring the Instagram Graph API product in the Facebook App Dashboard
  • Implementing OAuth token exchange flow to obtain long-lived access credentials
  • Updating Lambda environment variables with Graph API credentials
  • Establishing a token refresh strategy for 60-day credential rotation

Technical Details: Architecture Overview

The system uses a three-tier architecture:

  • Frontend: Static HTML at /tools/shipcaptaincrew/index.html served by CloudFront
  • API Layer: AWS Lambda function shipcaptaincrew in us-east-1 (account 782785212866) handling guest photo queries and Instagram media fetching
  • Data: Guest-approved photos stored in DynamoDB; Instagram data fetched on-demand via Graph API

When a user visits /g/2026-04-29, the frontend makes an API call to the Lambda handler. The Lambda function:

  1. Queries DynamoDB for approved photos matching the event_id (date)
  2. Calls Instagram Graph API with the stored IG_USER_ID and IG_ACCESS_TOKEN environment variables
  3. Filters Instagram posts by timestamp to match the charter window
  4. Returns a merged JSON response combining both photo sources

Infrastructure Changes: Facebook App Configuration

The core issue was that the Lambda function referenced environment variables (IG_USER_ID, IG_ACCESS_TOKEN) that were empty. The app had a "Manage messaging" product configured, which grants Direct Message permissions but not the instagram_basic scope required to read media.

The fix required adding the correct product to the Facebook App (sailjada-social, ID: 1688884572116630):

  • Navigate to app dashboard → Add Product
  • Select Instagram Graph API (distinct from Basic Display or Messaging APIs)
  • This grants access to media endpoints and scopes needed for /ig_hashtag_search and /media queries

The @sailjada Instagram account (a Business/Creator account) needed to be explicitly connected to the app:

  • Within Instagram Graph API settings → API setup with Instagram login
  • Authenticate as @sailjada to authorize the app to read its media
  • This creates the linkage between the Instagram account and the Facebook Page managing the app

OAuth Token Flow: Short-lived to Long-lived Exchange

Facebook's Instagram Graph API uses a two-tier token system. The process:

# Step 1: Generate short-lived token (valid ~2 hours)
# Using Graph API Explorer at developers.facebook.com/tools/explorer
# - Select app: sailjada-social  
# - Select FB Page linked to @sailjada
# - Request scopes: instagram_basic, pages_show_list
# Token appears in the explorer interface

# Step 2: Retrieve IG_USER_ID from the FB Page
curl -s "https://graph.instagram.com/v18.0/{PAGE_ID}/instagram_business_account?access_token={SHORT_LIVED_TOKEN}" \
  | jq '.instagram_business_account.id'
# Returns: 17841400000000000 (example format)

# Step 3: Exchange short-lived token for long-lived token (valid ~60 days)
curl -s "https://graph.instagram.com/v18.0/oauth/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'
# Returns: long-lived token string

Once obtained, these two values were stored as Lambda environment variables:

  • IG_USER_ID — the Instagram Business Account ID
  • IG_ACCESS_TOKEN — the long-lived token (60-day validity)

The Lambda function then uses these to call the media endpoint:

# Inside lambda_function.py handler
import os
import requests

ig_user_id = os.environ.get('IG_USER_ID')
ig_token = os.environ.get('IG_ACCESS_TOKEN')

url = f"https://graph.instagram.com/v18.0/{ig_user_id}/media"
params = {
    'fields': 'id,caption,media_type,media_url,timestamp',
    'access_token': ig_token
}
response = requests.get(url, params=params)
media_list = response.json().get('data', [])

Updating Lambda Configuration

Environment variables were updated using the AWS CLI:

aws lambda update-function-configuration \
  --function-name shipcaptaincrew \
  --region us-east-1 \
  --environment Variables={IG_USER_ID=17841400000000000,IG_ACCESS_TOKEN=IGABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz...}

The Lambda function file at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py already contained the logic to fetch and filter Instagram media; it was simply waiting for valid credentials.

Key Design Decisions

Why use long-lived tokens instead of refreshing via user flow? Instagram Graph API long-lived tokens remain valid for 60 days without user interaction. Since this is a backend service (not a user-facing OAuth flow), a long-lived token stored in Lambda environment variables is appropriate. If the token expires in production, it's refreshed using the same exchange endpoint with APP_ID and APP_SECRET—no user re-authentication needed.

Why filter by timestamp in Lambda rather than at query time? Instagram's Graph API doesn't accept timestamp range filters on the media endpoint. Filtering in Lambda is simpler than building a separate indexing layer and allows flexibility to adjust the time window (e.g., ±2 hours around the charter).

Why not cache Instagram data? The dataset is small (typically 5–15 posts per charter date). Fetching on-demand keeps the guest gallery fresh without adding DynamoDB writes or cache invalidation logic.

Testing and Verification

After updating environment variables, the guest gallery was tested at: