```html

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_basic and pages_show_list scopes, 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 ID
  • IG_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.

No