Integrating Instagram Graph API into a Lambda-Based Photo Gallery: Architecture and Implementation

The guest photo page system at shipcaptaincrew.queenofsandiego.com/g/{event_id} was designed to surface curated content from two sources: user-uploaded charter photos and organic Instagram posts from the official @sailjada account. While the Lambda function had dormant Instagram integration logic, the Graph API credentials were never configured. This post walks through the complete setup, architecture decisions, and the specific gotchas we encountered.

What Was Done

We activated Instagram Graph API integration for the shipcaptaincrew Lambda function (us-east-1, account 782785212866) to automatically fetch and display @sailjada posts within a configurable time window on the guest photo page. The integration uses long-lived access tokens (60-day expiry) stored as Lambda environment variables, with a refresh strategy to prevent token expiration.

Technical Architecture

The existing Lambda function contains conditional logic that returns an empty array when IG_USER_ID or IG_ACCESS_TOKEN environment variables are absent. The dormant code path makes authenticated requests to the Instagram Graph API endpoint:

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

The Lambda filters results by timestamp to match the event window, then merges them with approved guest photos for display on the page. This architecture avoids storing Instagram data in a database—each page view triggers a fresh API call, ensuring near real-time content.

Why This Approach?

We chose to read directly from Instagram's API rather than polling and caching for several reasons:

  • Freshness: Posts published hours before a guest views the page appear immediately, without requiring a separate ingestion job.
  • Simplicity: No additional storage layer (DynamoDB, S3) needed for Instagram metadata.
  • Cost efficiency: Graph API calls are free; caching would require additional AWS resources and maintenance.
  • Compliance: Each request is authenticated with a single token, reducing the surface area for credential exposure.

The Critical Setup Issue: Product Selection

The initial setup attempted to use Instagram's Messaging product, which grants permissions for direct message functionality but does NOT include the instagram_basic scope required to read media. This is a common gotcha: Facebook's product-permission model is granular. To read media, you must add the Instagram Graph API product explicitly, separate from Messaging.

Step-by-Step Implementation

1. Add Instagram Graph API Product

In the Facebook App Dashboard for sailjada-social (created under account 782785212866):

  • Navigate to Add Product (lower left sidebar)
  • Search for and select Instagram Graph API (not Basic Display, not Messaging)
  • Click Set Up and confirm the product is now in your left sidebar

2. Verify @sailjada Account Status

The @sailjada Instagram account must be a Business or Creator account linked to a Facebook Page. This is a prerequisite; personal accounts cannot be accessed via Graph API. Verify in Instagram settings: Profile → Switch to Professional Account → Category (Creator or Business).

3. Connect the Instagram Account to Your App

Within the Instagram Graph API product settings:

  • Click API Setup with Instagram login
  • Click Add Instagram account
  • Log in with the @sailjada credentials
  • Authorize the app to access media, insights, and basic info

4. Generate a Short-Lived Access Token

Use the Graph API Explorer:

  • Select the sailjada-social app from the dropdown
  • Click Generate Access Token
  • Select the Facebook Page linked to @sailjada
  • Ensure the requested scopes include instagram_basic and pages_show_list
  • Copy the generated token (valid for ~2 hours)

5. Retrieve the IG_USER_ID

Using the short-lived token from step 4, make two Graph API calls. First, get the Facebook Page's Instagram business account:

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

The returned id is your IG_USER_ID. Store this value; it remains constant and does not expire.

6. Exchange for a Long-Lived Access Token

Short-lived tokens expire in hours. Exchange it for a long-lived token (60-day expiry):

curl -s "https://graph.instagram.com/v18.0/oauth/access_token" \
  -d "grant_type=ig_refresh_token" \
  -d "access_token={SHORT_LIVED_TOKEN}" | jq '.access_token'

The returned access_token is your IG_ACCESS_TOKEN. This is what you'll store in Lambda.

7. Update Lambda Environment Variables

Using the AWS CLI or Console, update the shipcaptaincrew function's environment variables:

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

No restart is required; environment variables take effect on the next invocation.

Token Refresh Strategy

Long-lived tokens expire after 60 days. We recommend one of two approaches:

  • Manual refresh: Repeat step 6 monthly, update Lambda environment variables via CLI or Console.
  • Automated refresh: Create an EventBridge rule (cron: 0 0 1 * ? *) that triggers a Lambda function to refresh the token and update shipcaptaincrew's environment variables using update-function-configuration. This eliminates manual intervention.

For the automated approach, the refresh Lambda would require IAM permissions for lambda:UpdateFunctionConfiguration on shipcaptaincrew.

Verification

After updating environment variables, visit https://shipcaptaincrew.queenofsandiego.com/g/2026-04-29 (or any past event date). The page should now display Instagram posts from @sailjada alongside