Instagram Graph API Integration Guide
Version: 2025 Last Updated: October 3, 2025 API Version: Graph API v23.0+
Table of Contents
- Overview
- Instagram App Setup
- Authentication Flow (OAuth 2.0)
- Access Token Management
- API Endpoints
- Rate Limits & Quotas
- Error Handling
- Business Account Requirements
- Implementation Examples
Overview
IMPORTANT: As of December 4, 2024, the Instagram Basic Display API has been officially deprecated. All new implementations must use the Instagram Graph API for business accounts.
Key Features
- Access to business/creator account media
- Long-lived access tokens (60-day validity)
- Supports all media types: IMAGE, VIDEO, CAROUSEL_ALBUM, REELS
- Pagination support for large datasets
- Automatic token refresh capability
Prerequisites
- Instagram Business or Creator account
- Facebook Page connected to the Instagram account
- Facebook account linked to both
- Meta Developer App with Instagram permissions
Instagram App Setup
Step 1: Create a Facebook App
- Visit Meta for Developers
- Create a new app (select "Business" type)
- Add Instagram Graph API product to your app
Step 2: Configure App Settings
App ID & Secret
After creating your app, you'll receive:
- App ID (Client ID):
INSTAGRAM_CLIENT_ID - App Secret (Client Secret):
INSTAGRAM_CLIENT_SECRET
Add these to your .env:
INSTAGRAM_CLIENT_ID=your_app_id
INSTAGRAM_CLIENT_SECRET=your_app_secret
INSTAGRAM_REDIRECT_URI=https://yourdomain.com/instagram/callback
OAuth Redirect URIs
Configure valid OAuth redirect URIs in your app settings:
- Development:
http://localhost/instagram/callbackorhttps://yourapp.test/instagram/callback - Production:
https://yourdomain.com/instagram/callback
Important: The redirect URI must match exactly (including protocol and trailing slashes).
Step 3: Configure Permissions
Required permissions for your app:
instagram_business_basic- Read basic business account info and mediainstagram_business_content_publish- Optional, for publishing featuresinstagram_business_manage_messages- Optional, for message managementinstagram_business_manage_comments- Optional, for comment management
Step 4: App Review (Production)
For production use, submit your app for review to access:
- Public data from any Instagram Business account
- Higher rate limits
- Extended permissions
Authentication Flow (OAuth 2.0)
Flow Diagram
User clicks "Connect Instagram"
↓
Redirect to Instagram OAuth
↓
User authorizes app
↓
Callback with authorization code
↓
Exchange code for short-lived token
↓
Exchange short-lived for long-lived token
↓
Store token + expiry in database
Step 1: Redirect to Instagram OAuth
Using Laravel Socialite:
use Laravel\Socialite\Facades\Socialite;
Route::get('/instagram/redirect', function () {
return Socialite::driver('instagram')
->setScopes(['instagram_business_basic'])
->redirect();
})->name('instagram.redirect');
Manual OAuth URL:
https://www.facebook.com/v23.0/dialog/oauth?
client_id={app-id}
&redirect_uri={redirect-uri}
&scope=instagram_business_basic
&response_type=code
&state={csrf-token}
Step 2: Handle OAuth Callback
Receives authorization code from Instagram:
Route::get('/instagram/callback', function () {
if (!request()->has('code') || request('error')) {
return redirect()->route('dashboard')
->with('error', 'Instagram authorization failed');
}
// Get user data and short-lived token via Socialite
$user = Socialite::driver('instagram')->user();
// Exchange for long-lived token (next step)
$longLivedToken = exchangeForLongLivedToken($user->token);
// Store account details
Account::create([
'user_id' => auth()->id(),
'instagram_user_id' => $user->getId(),
'username' => $user->getName(),
'access_token' => encrypt($longLivedToken['access_token']),
'expires_at' => now()->addSeconds($longLivedToken['expires_in']),
]);
return redirect()->route('dashboard');
});
Step 3: Exchange for Long-Lived Token
Endpoint:
GET https://graph.instagram.com/access_token
Parameters:
grant_type:ig_exchange_tokenclient_secret: Your app secretaccess_token: Short-lived token from OAuth
Example Request:
use Illuminate\Support\Facades\Http;
function exchangeForLongLivedToken(string $shortLivedToken): array
{
$response = Http::get('https://graph.instagram.com/access_token', [
'grant_type' => 'ig_exchange_token',
'client_secret' => config('services.instagram.client_secret'),
'access_token' => $shortLivedToken,
]);
return $response->json();
// Returns: ['access_token' => '...', 'expires_in' => 5183944]
}
Response:
{
"access_token": "long_lived_token_here",
"token_type": "bearer",
"expires_in": 5183944
}
expires_in is approximately 60 days in seconds
Access Token Management
Token Lifecycle
| Stage | Duration | Action Required |
|---|---|---|
| Short-lived token | ~1 hour | Exchange immediately |
| Long-lived token | 60 days | Refresh before expiry |
| Expired token | N/A | Re-authenticate user |
Refresh Long-Lived Token
Important: Tokens must be:
- At least 24 hours old
- Not yet expired
- Refreshed at least once every 60 days
Endpoint:
GET https://graph.instagram.com/refresh_access_token
Parameters:
grant_type:ig_refresh_tokenaccess_token: Current long-lived token
Example Implementation:
public function refreshAccessToken(): void
{
$response = Http::get('https://graph.instagram.com/refresh_access_token', [
'grant_type' => 'ig_refresh_token',
'access_token' => decrypt($this->access_token),
]);
$data = $response->json();
$this->update([
'access_token' => encrypt($data['access_token']),
'expires_at' => now()->addSeconds($data['expires_in']),
]);
}
Response:
{
"access_token": "new_long_lived_token",
"token_type": "bearer",
"expires_in": 5183944
}
Automated Refresh Strategy
Recommended Approach:
- Create a scheduled job to run daily
- Find tokens expiring within 7 days
- Refresh each token
- Update database with new token and expiry
// In routes/console.php
Schedule::command('instagram:refresh-tokens')->daily();
// Command implementation
Account::query()
->where('status', 'active')
->where('expires_at', '<=', now()->addDays(7))
->where('expires_at', '>', now())
->each(function (Account $account) {
try {
$account->refreshAccessToken();
} catch (\Exception $e) {
// Mark as expired, notify user
$account->update(['status' => 'expired']);
Mail::to($account->user)->send(new TokenExpiredMail($account));
}
});
API Endpoints
1. Get User Media
Fetch all media for an Instagram business account.
Endpoint:
GET https://graph.instagram.com/{instagram-user-id}/media
Parameters:
fields: Comma-separated list of fields to returnlimit: Number of media items per page (default: 25, max: 100)after: Pagination cursor for next pagesince: Unix timestamp for time-based filteringuntil: Unix timestamp for time-based filtering
Available Fields:
id, caption, media_type, media_url, thumbnail_url, permalink,
timestamp, username, comments_count, like_count, children
Example Request:
use Illuminate\Support\Facades\Http;
public function fetchMedia(): void
{
$fields = [
'id',
'caption',
'media_type',
'media_url',
'thumbnail_url',
'permalink',
'timestamp',
'comments_count',
'like_count',
];
$response = Http::get("https://graph.instagram.com/{$this->instagram_user_id}/media", [
'fields' => implode(',', $fields),
'limit' => 100,
'access_token' => decrypt($this->access_token),
]);
$data = $response->json();
foreach ($data['data'] as $media) {
$this->media()->updateOrCreate(
['instagram_id' => $media['id']],
[
'caption' => $media['caption'] ?? '',
'media_type' => $media['media_type'],
'media_url' => $media['media_url'],
'thumbnail_url' => $media['thumbnail_url'] ?? null,
'permalink' => $media['permalink'],
'timestamp' => $media['timestamp'],
'comments_count' => $media['comments_count'] ?? 0,
'like_count' => $media['like_count'] ?? 0,
]
);
}
// Handle pagination if needed
if (isset($data['paging']['next'])) {
$this->fetchNextPage($data['paging']['next']);
}
}
Response Structure:
{
"data": [
{
"id": "17895695668004550",
"caption": "Beautiful sunset at the beach!",
"media_type": "IMAGE",
"media_url": "https://scontent.cdninstagram.com/...",
"permalink": "https://www.instagram.com/p/ABC123/",
"timestamp": "2025-01-15T10:30:00+0000",
"comments_count": 42,
"like_count": 384
},
{
"id": "17895695668004551",
"media_type": "VIDEO",
"media_url": "https://scontent.cdninstagram.com/...",
"thumbnail_url": "https://scontent.cdninstagram.com/...",
"permalink": "https://www.instagram.com/p/XYZ789/",
"timestamp": "2025-01-14T15:45:00+0000",
"comments_count": 23,
"like_count": 156
}
],
"paging": {
"cursors": {
"before": "QVFIUjRSa...",
"after": "QVFIUjA3b..."
},
"next": "https://graph.instagram.com/v23.0/.../media?after=QVFIUjA3b..."
}
}
Limitations:
- Returns maximum of 10,000 most recently created media
- Stories are not included (use separate
/storiesendpoint) - Only returns media created by the specified Instagram user
2. Get User Profile Information
Fetch Instagram business account details.
Endpoint:
GET https://graph.instagram.com/me
Available Fields:
id, username, name, profile_picture_url, followers_count,
follows_count, media_count, biography, website
Example Request:
$response = Http::get('https://graph.instagram.com/me', [
'fields' => 'id,username,name,profile_picture_url,followers_count,follows_count,media_count,biography',
'access_token' => decrypt($this->access_token),
]);
Response:
{
"id": "17841405793187218",
"username": "mycompany",
"name": "My Company",
"profile_picture_url": "https://scontent.cdninstagram.com/...",
"followers_count": 12453,
"follows_count": 342,
"media_count": 187,
"biography": "Official account for My Company"
}
3. Get Individual Media Details
Endpoint:
GET https://graph.instagram.com/{media-id}
Example Request:
$response = Http::get("https://graph.instagram.com/{$mediaId}", [
'fields' => 'id,caption,media_type,media_url,timestamp,children{media_url,media_type}',
'access_token' => $token,
]);
Note: For carousel albums, use children{media_url,media_type} to get all child media.
Rate Limits & Quotas
Platform Rate Limits
Instagram Graph API uses Business Use Cases (BUC) rate limiting:
Formula:
Calls per 24 hours = 4,800 × Number of Impressions
Where impressions = number of times any Instagram content has appeared on a person's screen in the last 24 hours.
Standard Limits
| Tier | Calls per User per Hour | Notes |
|---|---|---|
| Per User | 200 | Standard platform limit |
| Media Comments | 60 writes/hour | Special restriction |
| BUC Daily | 4,800 × impressions | Resets every 24 hours |
Important Notes:
- Rate limits are per app-user pair
- Higher engagement accounts = higher rate limits
- Limits reset every 24 hours
- Previous short-lived token had 5,000/hour (reduced to 200 in recent years)
Rate Limit Headers
Instagram returns these headers with API responses:
X-App-Usage: {"call_count":45,"total_cputime":25,"total_time":50}
X-Business-Use-Case-Usage: {"123456789":{"call_count":10,"total_cputime":5,"total_time":10}}
Handling Rate Limits
Detection:
- HTTP Status Code: 429 Too Many Requests
- Error Code: 4 or 17
- Error Type:
OAuthExceptionorAPIThrottleException
Best Practices:
- Implement exponential backoff
- Monitor usage headers
- Cache API responses (1 hour recommended)
- Batch requests when possible
- Queue jobs with proper delays
Example Implementation:
use Illuminate\Support\Facades\Cache;
public function fetchMediaWithRateLimit()
{
$cacheKey = "instagram_media_{$this->id}";
return Cache::remember($cacheKey, 3600, function () {
try {
return $this->fetchMedia();
} catch (\Exception $e) {
if ($this->isRateLimitError($e)) {
// Wait and retry with exponential backoff
sleep(60);
return $this->fetchMedia();
}
throw $e;
}
});
}
private function isRateLimitError(\Exception $e): bool
{
return str_contains($e->getMessage(), 'rate limit')
|| str_contains($e->getMessage(), '429');
}
Error Handling
Common Error Codes
| Code | Type | Description | Resolution |
|---|---|---|---|
| 4 | OAuthException | Rate limit exceeded | Wait and retry with backoff |
| 10 | OAuthException | Permission denied | Check app permissions |
| 17 | APIThrottleException | Too many calls | Implement rate limiting |
| 190 | OAuthException | Invalid/expired token | Refresh or re-authenticate |
| 200 | PermissionsError | Missing permission | Request instagram_business_basic |
| 100 | InvalidParameterException | Invalid parameter | Check API request format |
| 110 | InvalidUserException | Invalid user ID | Verify Instagram user ID |
| 2207013 | User Error Subcode | Username not found | Verify account exists |
Error Response Structure
{
"error": {
"message": "Application request limit reached",
"type": "OAuthException",
"code": 4,
"error_subcode": 2207013,
"fbtrace_id": "AXYZabc123"
}
}
Error Handling Strategy
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
public function handleInstagramApiError(\Exception $e, Account $account): void
{
$errorData = json_decode($e->getMessage(), true);
$errorCode = $errorData['error']['code'] ?? null;
Log::channel('instagram')->error('Instagram API Error', [
'account_id' => $account->id,
'error' => $errorData,
]);
match ($errorCode) {
190 => $this->handleExpiredToken($account),
4, 17 => $this->handleRateLimit($account),
200 => $this->handlePermissionError($account),
default => $this->handleGenericError($account, $errorData),
};
}
private function handleExpiredToken(Account $account): void
{
$account->update(['status' => 'expired']);
Mail::to($account->user)->send(new InstagramTokenExpiredMail($account));
}
private function handleRateLimit(Account $account): void
{
// Retry job later
RefreshInstagramToken::dispatch($account)->delay(now()->addHour());
}
Graceful Degradation
public function getMedia()
{
try {
return $this->fetchMediaFromInstagram();
} catch (\Exception $e) {
// Log error
Log::error('Instagram API failed', ['error' => $e->getMessage()]);
// Return cached data if available
return Cache::get("instagram_media_{$this->id}", collect());
}
}
Business Account Requirements
Account Type Validation
Required:
- Instagram Business or Creator account (personal accounts not supported)
- Facebook Page connected to Instagram account
- Facebook account linked to both
- Using
instagram_business_basicscope (notinstagram_basic)
How to Validate Account Type
When fetching user data, check the account type:
public function isBusinessAccount(): bool
{
try {
$response = Http::get('https://graph.instagram.com/me', [
'fields' => 'id,username,account_type',
'access_token' => decrypt($this->access_token),
]);
$data = $response->json();
// account_type should be 'BUSINESS' or 'CREATOR'
return in_array($data['account_type'] ?? '', ['BUSINESS', 'CREATOR']);
} catch (\Exception $e) {
return false;
}
}
Converting Personal to Business Account
Users must:
- Have a Facebook Page
- Go to Instagram Settings → Account → Switch to Professional Account
- Choose Business or Creator
- Link to Facebook Page
In-App Guidance:
if (!$account->isBusinessAccount()) {
return redirect()->back()->with('error',
'Please convert your Instagram account to a Business or Creator account. ' .
'Visit Instagram Settings → Account → Switch to Professional Account.'
);
}
Supported Media Types
Media Type Overview
| Type | Description | Required Fields | Optional Fields |
|---|---|---|---|
| IMAGE | Single photo | media_url |
thumbnail_url, caption |
| VIDEO | Single video | media_url, thumbnail_url |
caption |
| CAROUSEL_ALBUM | Multiple photos/videos | N/A (use children) | caption |
| REELS | Short-form video | media_url, thumbnail_url |
is_shared_to_feed |
Field Requirements by Type
IMAGE:
{
"id": "123",
"media_type": "IMAGE",
"media_url": "https://...",
"caption": "Photo caption",
"permalink": "https://instagram.com/p/...",
"timestamp": "2025-01-15T10:30:00+0000"
}
VIDEO:
{
"id": "456",
"media_type": "VIDEO",
"media_url": "https://video.cdninstagram.com/...",
"thumbnail_url": "https://scontent.cdninstagram.com/...",
"caption": "Video caption",
"permalink": "https://instagram.com/p/...",
"timestamp": "2025-01-14T15:45:00+0000"
}
CAROUSEL_ALBUM:
{
"id": "789",
"media_type": "CAROUSEL_ALBUM",
"caption": "Carousel caption",
"children": {
"data": [
{
"id": "789_1",
"media_type": "IMAGE",
"media_url": "https://..."
},
{
"id": "789_2",
"media_type": "VIDEO",
"media_url": "https://...",
"thumbnail_url": "https://..."
}
]
},
"permalink": "https://instagram.com/p/...",
"timestamp": "2025-01-13T12:00:00+0000"
}
Fetching Carousel Children
Use the children field with nested field selection:
$response = Http::get("https://graph.instagram.com/{$mediaId}", [
'fields' => 'id,media_type,caption,children{id,media_type,media_url,thumbnail_url}',
'access_token' => $token,
]);
Implementation Examples
Complete OAuth Flow Implementation
config/services.php:
'instagram' => [
'client_id' => env('INSTAGRAM_CLIENT_ID'),
'client_secret' => env('INSTAGRAM_CLIENT_SECRET'),
'redirect' => env('INSTAGRAM_REDIRECT_URI'),
],
Provider Configuration (for Socialite):
// In EventServiceProvider or AppServiceProvider
use SocialiteProviders\Instagram\InstagramExtendSocialite;
use SocialiteProviders\Manager\SocialiteWasCalled;
protected $listen = [
SocialiteWasCalled::class => [
InstagramExtendSocialite::class,
],
];
Complete Controller:
namespace App\Http\Controllers;
use App\Models\Account;
use App\Jobs\FetchInstagramMedia;
use Illuminate\Support\Facades\Http;
use Laravel\Socialite\Facades\Socialite;
class InstagramController extends Controller
{
public function redirect()
{
return Socialite::driver('instagram')
->setScopes(['instagram_business_basic'])
->redirect();
}
public function callback()
{
if (!request()->has('code') || request('error')) {
return redirect()->route('dashboard')
->with('error', 'Instagram authorization failed');
}
$instagramUser = Socialite::driver('instagram')->user();
// Exchange for long-lived token
$longLivedResponse = Http::get('https://graph.instagram.com/access_token', [
'grant_type' => 'ig_exchange_token',
'client_secret' => config('services.instagram.client_secret'),
'access_token' => $instagramUser->token,
]);
$longLivedData = $longLivedResponse->json();
// Create or update account
$account = Account::updateOrCreate(
[
'user_id' => auth()->id(),
'instagram_user_id' => $instagramUser->getId(),
],
[
'username' => $instagramUser->getName(),
'access_token' => encrypt($longLivedData['access_token']),
'expires_at' => now()->addSeconds($longLivedData['expires_in']),
'status' => 'active',
]
);
// Dispatch job to fetch media
FetchInstagramMedia::dispatch($account);
return redirect()->route('dashboard')
->with('success', 'Instagram account connected successfully!');
}
}
Token Refresh Command
namespace App\Console\Commands;
use App\Models\Account;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use App\Mail\InstagramTokenExpiredMail;
class RefreshInstagramTokens extends Command
{
protected $signature = 'instagram:refresh-tokens';
protected $description = 'Refresh Instagram access tokens expiring soon';
public function handle()
{
$accounts = Account::query()
->where('status', 'active')
->where('expires_at', '<=', now()->addDays(7))
->where('expires_at', '>', now())
->get();
$this->info("Found {$accounts->count()} accounts to refresh");
foreach ($accounts as $account) {
try {
$response = Http::get('https://graph.instagram.com/refresh_access_token', [
'grant_type' => 'ig_refresh_token',
'access_token' => decrypt($account->access_token),
]);
$data = $response->json();
$account->update([
'access_token' => encrypt($data['access_token']),
'expires_at' => now()->addSeconds($data['expires_in']),
]);
$this->info("Refreshed token for account: {$account->username}");
} catch (\Exception $e) {
$this->error("Failed to refresh {$account->username}: {$e->getMessage()}");
$account->update(['status' => 'expired']);
Mail::to($account->user)->send(new InstagramTokenExpiredMail($account));
}
}
$this->info('Token refresh completed');
}
}
Fetch Media Job
namespace App\Jobs;
use App\Models\Account;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class FetchInstagramMedia implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $backoff = [60, 300, 900]; // 1min, 5min, 15min
public function __construct(public Account $account)
{
}
public function handle(): void
{
$fields = [
'id',
'caption',
'media_type',
'media_url',
'thumbnail_url',
'permalink',
'timestamp',
'comments_count',
'like_count',
];
try {
$response = Http::get(
"https://graph.instagram.com/{$this->account->instagram_user_id}/media",
[
'fields' => implode(',', $fields),
'limit' => 100,
'access_token' => decrypt($this->account->access_token),
]
);
if (!$response->successful()) {
throw new \Exception($response->body());
}
$data = $response->json();
foreach ($data['data'] as $media) {
$this->account->media()->updateOrCreate(
['instagram_id' => $media['id']],
[
'caption' => $media['caption'] ?? '',
'media_type' => $media['media_type'],
'media_url' => $media['media_url'],
'thumbnail_url' => $media['thumbnail_url'] ?? null,
'permalink' => $media['permalink'],
'timestamp' => $media['timestamp'],
'comments_count' => $media['comments_count'] ?? 0,
'like_count' => $media['like_count'] ?? 0,
]
);
}
$this->account->update(['last_fetch_at' => now()]);
// Handle pagination if needed
if (isset($data['paging']['next'])) {
$this->fetchNextPage($data['paging']['next']);
}
} catch (\Exception $e) {
Log::channel('instagram')->error('Failed to fetch media', [
'account_id' => $this->account->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
private function fetchNextPage(string $nextUrl): void
{
// Implement pagination logic if needed
// For MVP, limiting to first 100 might be sufficient
}
}
Additional Resources
Official Documentation
Helpful Tools
- Graph API Explorer - Test API calls
- Access Token Debugger - Debug tokens
- Meta Business Suite - Manage connected accounts
Testing Considerations
- Use sandbox/test Instagram accounts for development
- Mock Instagram API responses in tests
- Test token expiry scenarios
- Test rate limit handling
- Test all media types (IMAGE, VIDEO, CAROUSEL_ALBUM)
Summary
Key Takeaways
- Instagram Basic Display API is deprecated - Use Instagram Graph API
- Business accounts only - Personal accounts not supported
- Tokens expire in 60 days - Must refresh at least every 60 days
- Rate limits are dynamic - Based on Business Use Cases (4,800 × impressions per day)
- Pagination required - Maximum 10,000 media items returned
- Error handling critical - Implement retry logic and user notifications
Implementation Checklist
- Create Facebook App with Instagram Graph API product
- Configure OAuth redirect URIs
- Implement OAuth flow with Socialite
- Exchange short-lived for long-lived tokens
- Store encrypted tokens in database
- Implement token refresh command (scheduled daily)
- Create job for fetching media
- Implement rate limit handling with backoff
- Add error logging and user notifications
- Validate business account type
- Handle all media types (IMAGE, VIDEO, CAROUSEL_ALBUM)
- Test token expiry and refresh flows
Document Version: 1.0 Last Reviewed: October 3, 2025 Next Review: January 3, 2026