```html

Integrating Instagram Graph API into a Lambda-based Guest Photo Gallery

Building a photo gallery that merges user-uploaded charter images with official Instagram posts requires careful orchestration between AWS Lambda, S3, and Meta's Graph API. This post covers the technical architecture, implementation details, and operational decisions made to integrate Instagram's media endpoint into the Ship Captain Crew guest photo system.

What Was Done

The Ship Captain Crew application at shipcaptaincrew.queenofsandiego.com displays guest-uploaded photos alongside curated Instagram posts from @sailjada. Previously, the Instagram integration was dormant—the Lambda function contained the plumbing but returned empty arrays because environment variables weren't configured. This work activates that integration by establishing proper OAuth2 token exchange with Meta's API.

  • Created Instagram Graph API product configuration in the sailjada-social Meta app
  • Implemented OAuth2 short-lived to long-lived token exchange workflow
  • Updated Lambda environment variables: IG_USER_ID and IG_ACCESS_TOKEN
  • Designed a 60-day token refresh strategy using AWS EventBridge (optional, scoped for future)
  • Verified end-to-end integration at /g/{event_id} routes

Technical Architecture

API Integration Flow

The Lambda function lambda_function.py in /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/ contains a get_instagram_posts() function that:

  • Accepts an ISO date string (e.g., "2026-04-29")
  • Queries the Instagram Graph API endpoint: https://graph.instagram.com/v19.0/{IG_USER_ID}/media
  • Filters results by timestamp window (same day, with timezone awareness)
  • Returns post metadata: image URL, caption, like count, timestamp
  • Gracefully degrades (returns empty array) if credentials are missing

The front-end index.html renders both photo streams in a unified gallery. The guest photo stream is populated from DynamoDB (approved uploads), while the Instagram stream is populated dynamically from the Lambda response.

OAuth2 Token Lifecycle

Meta's Instagram Graph API uses OAuth2 with two token types:

  • Short-lived tokens: Valid for ~1 hour, generated via Graph API Explorer or manual authorization flow
  • Long-lived tokens: Valid for ~60 days, exchanged via app credentials (APP_ID + APP_SECRET)

The decision to use long-lived tokens (rather than implementing a full OAuth2 authorization flow) reflects our constraints: this is a server-to-server integration without user-facing login. Credentials are stored as Lambda environment variables.

Implementation Details

Step 1: Meta App Configuration

The sailjada-social Meta app required the Instagram Graph API product, not the Messaging product (which was initially added for DM use cases). Product setup path:

Meta App Dashboard
  → sailjada-social
    → Add Product
      → Instagram → Instagram Graph API (not Basic Display)

Within Instagram Graph API settings, we registered the @sailjada Instagram business account. This account must be linked to a Facebook Page to access media insights and the Graph API.

Step 2: Token Generation via Graph API Explorer

Using the Meta developers tool at developers.facebook.com/tools/explorer:

1. Select app: sailjada-social
2. Select page: [Facebook Page linked to @sailjada]
3. Generate Access Token
4. Request scopes: instagram_basic, pages_show_list

This yields a short-lived token valid for ~1 hour.

Step 3: Retrieve IG_USER_ID

The Instagram user ID (distinct from the Instagram username) is required for all subsequent API calls. Using the short-lived token:

curl -s "https://graph.facebook.com/v19.0/{PAGE_ID}?fields=instagram_business_account&access_token={SHORT_LIVED_TOKEN}" \
  | jq '.instagram_business_account.id'

The returned ID becomes IG_USER_ID (environment variable).

Step 4: Exchange for Long-Lived Token

Using app credentials (APP_ID from app settings, APP_SECRET from app dashboard):

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 token is valid for 60 days and is stored as IG_ACCESS_TOKEN.

Infrastructure & Deployment

Lambda Configuration

AWS Lambda function: shipcaptaincrew, region us-east-1, account 782785212866.

Environment variables set via AWS Lambda console or CLI:

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

The function handler processes two request types:

  • GET /g/{event_id} → renders index.html with guest photo metadata
  • GET /api/instagram/{event_id} → JSON endpoint returning Instagram posts

Both routes are served via CloudFront distribution, which caches HTML at 3600 seconds (configurable per route pattern).

Monitoring & Logs

CloudWatch logs are available at /aws/lambda/shipcaptaincrew. The get_instagram_posts() function logs API call timing and any errors (missing credentials, API failures, invalid dates).

Key Technical Decisions

  • Long-lived tokens over refresh tokens: Simpler operational model. 60-day expiration is acceptable for an internal integration. Refresh strategy (EventBridge automation) is scoped for future work if manual monthly renewal becomes burdensome.
  • Environment variables for credentials: Avoids hardcoding; integrates with AWS Secrets Manager if needed later.
  • Timezone-aware filtering: Event IDs are ISO dates (e.g., "2026-04-29"), but Instagram timestamps are UTC. The Lambda function converts the date to a time window and accounts for timezone offset (configured in environment or computed from event metadata).
  • Graceful degradation: If credentials are missing, get_instagram_posts() returns [] rather than failing. The