# Instagram API with Unified Login: Integration Guide

Integrate Instagram API with Unified Login from config_id to publishing: exchange codes, discover accounts, subscribe webhooks, and handle token failures.

Canonical page: https://www.tryadeli.com/blog/instagram-api-unified-login-integration
Last updated: 2026-11-06

By Nick Reyes, Co-founder and CTO, Adeli. Published 2026-11-06.

## The short answer

To integrate Instagram API with Unified Login, create a Facebook Login for Business configuration, authorize with its configuration ID, and exchange the code on your server. Discover the shared Instagram accounts, select the intended account, and subscribe it to webhooks before testing features. Adeli handles this provider connection for teams that want Instagram publishing without maintaining their own Meta integration.

**Integrating Instagram API with Unified Login** means configuring consent in Meta, exchanging an authorization code for a system-user token, and discovering the Instagram accounts that token can access. You then subscribe the selected account to webhooks and use its ID for feature calls on `graph.facebook.com`.

This guide follows the sequence we implemented when moving Adeli to Unified Login in October 2026. Examples use generic identifiers and server-side fetch. Treat them as building blocks; you still need your own OAuth server around them. Read the [definition of Unified Login](https://www.tryadeli.com/blog/instagram-api-unified-login) for terminology or the [migration checklist](https://www.tryadeli.com/blog/migrate-to-instagram-api-unified-login) before changing existing connections.

## What do I need before starting the integration?

**Unified Login** requires a portfolio-owned Meta app using Facebook Login for Business, plus an eligible Instagram account and HTTPS endpoints. Meta's [get started guide](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-unified-login/get-started) lists the prerequisites.

- **App setup.** Add the Manage messaging & content on Instagram use case and make sure you have an admin role. Apps still on classic Facebook Login must switch to Facebook Login for Business.
- **Test account.** Use a professional Instagram account and accept its Instagram Tester invitation. Meta does not support test users for this flow.
- **Endpoints.** Prepare an HTTPS callback for OAuth and an HTTPS webhook receiver that verifies notifications.
- **Outside customers.** Get Advanced Access to the configured permissions and put the app in Live mode. Standard Access supports testing with app-role users.

If owning the app and review is the blocker, [Adeli's Instagram integration](https://www.tryadeli.com/product/instagram) removes those responsibilities from your team. The remaining steps are for developers maintaining a direct Meta integration.

## Common mistakes: what are developers running into?

Developers report **missing setup controls** in new apps and **account discovery failures** after consent succeeds. Each excerpt points to a different check to run before you treat a token as a working connection.

> the option "API setup with Instagram login" **does not appear in any newly created app**. Only "API setup with Facebook login" is offered.
>
> — [Dei on the Meta Developer Community](https://developers.facebook.com/community/threads/1078182901510168/), October 7, 2026

> The returned token passes app ID, SYSTEM_USER type, scope and client_business_id checks, but GET /me/assigned_instagram_accounts?fields=id,username **returns data:[]**.
>
> — [Isha on the Meta Developer Community](https://developers.facebook.com/community/threads/2091801151454102/), October 28, 2026

> So the asset assignment is recorded in Business Manager, but it is **not exposed to the system user token**.
>
> — [Lorenzo Pascucci on the Meta Developer Community](https://developers.facebook.com/community/threads/1683224160005163/), November 2, 2026

Meta's migration guide explains the missing Instagram Login setup for new apps. The two empty-account reports have no confirmed resolution; Steps 5–6 separate documented checks from conditions your app must reject safely.

## Step 1: Create the Unified Login configuration

The **Facebook Login for Business configuration** controls the requested assets, permissions, and token lifetime. Open **Use cases → Manage messaging & content on Instagram → Customize → API setup with Unified login** and create a configuration, following Meta's get started guide.

1. **Choose the token settings.** Confirm System-user access token and inspect its expiration. For Adeli, we selected a non-expiring token rather than scheduling a 60-day Instagram User token refresh.
2. **Choose the assets.** We requested Instagram accounts only and checked **Asset Required**, so the flow could not finish without sharing an Instagram asset. We chose that setting ourselves; it doesn't mean you have to add an ad account.
3. **Review permissions.** The default includes ads permissions. Remove permissions outside your product's use case and add the grants your features need.
4. **Save the redirect.** Enter your HTTPS callback, select the configuration with **Use this configuration**, and copy its login URL and `config_id`.

| Feature | Permission to configure |
|---|---|
| Profile and media reads | `instagram_basic` |
| Receiving and replying to DMs | `instagram_manage_messages` |
| Comment moderation | `instagram_manage_comments` |
| Publishing | `instagram_content_publish` |
| Insights | `instagram_manage_insights` |
| BISU token management API | `business_management` |

The [permissions reference](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-unified-login/permissions) distinguishes these grants. We included `business_management` to keep a future token-management path open; no Adeli Instagram feature required it, and we did not implement rotation as part of this migration.

Configure the webhook callback and verify token on the Unified Login tab. Select fields your receiver handles: Adeli used `messages`, `comments`, and `messaging_postbacks`. Callback verification must return the challenge after checking the verify token; event handling should validate the signature against the raw request body, as Meta's [webhook setup guide](https://developers.facebook.com/documentation/instagram-platform/webhooks/setup) describes.

## Step 2: Build the login URL with the configuration ID

The **Unified Login authorization URL** identifies the configuration through config_id; Adeli sends no scope parameter. Start from Meta's generated URL and bind an unpredictable, single-use `state` to the initiating session and intended customer.

```typescript
const login = new URL('https://www.facebook.com/dialog/oauth');
login.search = new URLSearchParams({
  client_id: metaAppId,
  config_id: configurationId,
  redirect_uri: redirectUri,
  response_type: 'code',
  prompt: 'login',
  state: singleUseState,
}).toString();
// Redirect the browser to login.toString().
```

Meta documents `prompt=login` to show both Instagram and Facebook choices even when the browser is already signed into Facebook. Adeli's shared authorization helper used `auth_type=rerequest` to request previously declined permissions again; that parameter does not replace the configuration.

At the callback, validate and consume `state` before exchanging the code. Handle cancellation explicitly and keep the target customer bound to the server-side session. A successful login must not let a caller substitute a different customer ID in the callback.

## Step 3: Exchange the code on your server

**Exchange the authorization code** once using the Meta app ID and secret, not the separate Instagram app credentials. Meta's [authentication guide](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-unified-login/authentication) specifies the Facebook Graph exchange.

The snippets share `graphBase`, `token`, and `igId`. Pick the Graph version on purpose from the ones your app supports, instead of copying one from an unrelated tutorial.

```typescript
const graphBase = `https://graph.facebook.com/${graphVersion}`;
const exchangeUrl = new URL(`${graphBase}/oauth/access_token`);
exchangeUrl.search = new URLSearchParams({
  client_id: metaAppId,
  client_secret: metaAppSecret,
  redirect_uri: redirectUri,
  code,
}).toString();
const response = await fetch(exchangeUrl, {
  signal: AbortSignal.timeout(15_000),
});
const payload = await response.json();
if (!response.ok || payload.error ||
    typeof payload.access_token !== 'string' || !payload.access_token) {
  throw new Error('Code exchange failed');
}
const token: string = payload.access_token;
const expiresAt = typeof payload.expires_in === 'number' &&
  Number.isFinite(payload.expires_in) && payload.expires_in > 0
  ? new Date(Date.now() + payload.expires_in * 1000)
  : null;
```

Adeli handled an absent positive `expires_in` as `null` because our configuration was non-expiring. Meta's Unified Login page does not promise an exchange response with that field, so **confirm your configured lifetime** rather than interpreting every missing field as perpetual access. There is no second `ig_exchange_token` or `fb_exchange_token` hop in our Unified Login flow.

Store the credential encrypted on the server. Redact the exchange URL from request logging because the secret is in its query string, and keep tokens out of analytics and browser responses.

## Step 4: Read the business identity and permissions

Store the **client business ID** alongside the system-user token, and keep the permissions you observed separate from the ones you configured. Under Unified Login, `/me` identifies the system user rather than the Instagram account.

Use a helper that rejects both HTTP and Graph errors without serializing tokens into error messages:

```typescript
async function graphGet(path: string, fields?: string, after?: string) {
  const url = new URL(`${graphBase}/${path}`);
  if (fields) url.searchParams.set('fields', fields);
  if (after) url.searchParams.set('after', after);
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` },
    signal: AbortSignal.timeout(15_000),
  });
  const body = await response.json();
  if (!response.ok || body.error) {
    throw Object.assign(new Error('Graph request failed'), {
      code: body.error?.code,
      subcode: body.error?.error_subcode,
    });
  }
  return body;
}
const business = await graphGet('me', 'client_business_id');
const clientBusinessId = business.client_business_id ?? null;
```

Meta documents `client_business_id` for its separate BISU token-management API. Do not mistake `business.id`, the system user's ID, for the Instagram account ID.

We also tried `GET /me/permissions` and kept entries whose `status` was `granted`. If the request failed or returned no grants, we recorded the configuration's expected permission list and logged **which source supplied it**.

```typescript
// Mirror the saved configuration; do not label this list as observed consent.
const configuredPermissions = [
  'instagram_basic', 'instagram_manage_messages',
  'instagram_manage_comments', 'instagram_content_publish',
  'instagram_manage_insights',
];
let grants: string[] = [];
try {
  const result = await graphGet('me/permissions');
  grants = (result.data ?? [])
    .filter((entry: { status?: string }) => entry.status === 'granted')
    .map((entry: { permission: string }) => entry.permission);
} catch {
  // Record a sanitized diagnostic, never the request token.
}
const permissionEvidence = grants.length
  ? { source: 'observed', permissions: grants }
  : { source: 'configuration', permissions: configuredPermissions };
```

We added that fallback defensively. Meta's Unified Login docs do not specify this grants response, and our migration plan leaves the live probe unresolved. **A configured permission is not proof of customer consent.** Keep the source distinction, handle permission errors at feature use, and ask for reconnection when a required grant is missing.

## Step 5: Discover every shared Instagram account

**Read assigned Instagram accounts** with pagination before deciding which account to connect. Meta's get started guide documents `/me/assigned_instagram_accounts?fields=id,username`, including cursor fields rather than a guaranteed `next` URL.

```typescript
const accounts = new Map<string, { id: string; username?: string }>();
const cursors = new Set<string>();
let after: string | undefined;
let complete = false;
for (let page = 0; page < 100; page++) {
  const result = await graphGet(
    'me/assigned_instagram_accounts', 'id,username', after,
  );
  if (!Array.isArray(result.data)) throw new Error('Invalid account response');
  for (const account of result.data) {
    if (account.id != null) accounts.set(String(account.id), {
      id: String(account.id), username: account.username,
    });
  }
  const next = result.paging?.cursors?.after;
  if (!result.data.length || !next || next === after) {
    complete = true;
    break;
  }
  if (cursors.has(next)) throw new Error('Repeated account cursor');
  cursors.add(next);
  after = next;
}
if (!complete) throw new Error('Account discovery exceeded safety limit');
```

The 100-page cap is a safety limit for this example; Meta does not set one. Don't silently select from incomplete results. A cursor may remain on a final nonempty page, so an additional empty page can finish discovery.

If the list is empty, check whether the account is professional and belongs to the selected portfolio. Meta's [troubleshooting page](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-unified-login/eligibility-and-troubleshooting) documents those checks. If they pass, collect the app ID, configuration ID, attempt time and timezone, login method, failing step, and visible error for support; the forum reports above don't point to a further fix.

## Step 6: Select an account and read its profile

Select the customer's **intended Instagram account** explicitly; never assume the first result is correct. Adeli supported one Instagram connection per profile and implemented this rule:

1. **No accounts:** stop and explain that no professional account was shared.
2. **One account:** connect that account.
3. **Several accounts:** retain the existing account if its ID matches a result; otherwise ask the customer to share only the intended account.

A product supporting several accounts can build a picker instead. Bind the selected ID to the authorized customer and verify it belongs to the discovered set.

```typescript
const profile = await graphGet(
  igId, 'id,username,name,profile_picture_url,followers_count,media_count',
);
```

Save the chosen identity with its token, portfolio ID, configuration ID, and expiry semantics. Adeli's Facebook-host profile read omitted Instagram Login's `user_id` and `account_type` fields; copying the old profile request wholesale would request fields our Facebook-host implementation did not use.

## Step 7: Subscribe the selected account to webhooks

After dashboard setup, **subscribe the individual account** with its system-user token. Instagram Login subscriptions do not transfer to Unified Login, according to Meta's [Instagram Login migration guide](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-instagram-login/migration-guide).

```bash
curl -X POST "https://graph.facebook.com/$GRAPH_VERSION/$IG_ID/subscribed_apps" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "subscribed_fields=messages,comments,messaging_postbacks"

curl "https://graph.facebook.com/$GRAPH_VERSION/$IG_ID/subscribed_apps" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Set those shell variables locally; do not paste real credentials into shared examples. A successful subscription returns `{"success":true}`. Verify the fields with GET and trigger a real event on the account.

Adeli made subscription best-effort after saving the connection and logged failures prominently. For your product, track subscription health separately so a saved token does not hide broken message delivery. Our code comments also noted that replies failed as outside the messaging window when the account subscription was missing. Confirm events arrive before diagnosing that error as a timestamp problem.

## Step 8: Make publishing, comments, and messaging calls

Feature requests use the **selected Instagram account** and the new token on `graph.facebook.com`. Test an operation requiring each permission rather than stopping after the profile read.

For a public JPEG, Meta's [content publishing guide](https://developers.facebook.com/documentation/instagram-platform/content-publishing) defines the container sequence:

```bash
curl -X POST "https://graph.facebook.com/$GRAPH_VERSION/$IG_ID/media" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "image_url=$PUBLIC_JPEG_URL" \
  --data-urlencode "caption=Testing our Instagram connection"

# Set CONTAINER_ID to the returned id; inspect readiness before publishing.
curl "https://graph.facebook.com/$GRAPH_VERSION/$CONTAINER_ID?fields=status_code" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Run only after status_code is FINISHED.
curl -X POST "https://graph.facebook.com/$GRAPH_VERSION/$IG_ID/media_publish" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "creation_id=$CONTAINER_ID"
```

Read comments on the resulting media, using the [comments reference](https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-media/comments):

```bash
curl "https://graph.facebook.com/$GRAPH_VERSION/$MEDIA_ID/comments?fields=id,text,timestamp" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

For messaging, the migration guide maps sends to `/{ig-user-id}/messages`. The [messaging guide](https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-instagram-login/messaging-api) defines the recipient and message body; apply the migration guide's host change. Reply to a recent eligible inbound conversation using its Instagram-scoped sender ID, not a username or Page ID:

```bash
curl -X POST "https://graph.facebook.com/$GRAPH_VERSION/$IG_ID/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"recipient":{"id":"<SENDER_IG_SCOPED_ID>"},"message":{"text":"Thanks for your message."}}'
```

Keep **Allow access to messages** enabled on the Instagram account. Unified Login does not remove messaging eligibility or permission checks. Use an inbound test message, confirm its webhook, then send the reply.

## What should happen when a token becomes invalid?

A Unified Login **token-invalid error** should move the connection into a reconnect-required state. Meta's authentication guidance asks clients to reconnect after removing the app or moving an account to another portfolio.

Adeli stopped the request on that error and did not retry through the legacy refresh flow. There is **no OAuth refresh grant** in this implementation. Meta separately documents a BISU token-management API; that is not Instagram Login's `ig_refresh_token` endpoint.

Keep missing-permission errors separate from invalid tokens, and handle transient network failures separately from both. A missing grant needs corrected consent; repeating the same request cannot add it.

## How do I know the integration is ready for customers?

**Verify a complete connection** and its failure paths before widening access. The minimum walkthrough should include:

1. **Consent:** both sign-in choices, intended portfolio, correct account, cancellation and invalid state.
2. **Features:** profile read, test publication, comment read, received DM and reply.
3. **Events:** account subscription, authentic notification, safe duplicate delivery.
4. **Recovery:** missing permission, empty or multiple accounts, app removal and reconnect.

If the Meta connection is infrastructure you would rather hand off, [Adeli's posting API](https://www.tryadeli.com/social-media-posting-api) covers Instagram publishing and scheduling with your API key. For inbox features, the [messaging API](https://www.tryadeli.com/social-media-messaging-api) provides received conversations and replies while Adeli maintains the underlying provider authorization.

## Frequently asked questions

### Should the Unified Login URL include scope?

Use the configuration ID to select permissions and assets. Adeli's Unified Login authorization URL sends no scope parameter. Editing a scope list in application code does not update the configuration saved in Meta's App Dashboard.

### Does a verified webhook callback subscribe every account?

No. Verifying the callback only sets up your receiver, and the App Dashboard selects event fields. After authorization, subscribe each shared Instagram account through its subscribed_apps edge using the system-user token, then check delivery from that account.

### What should I do when assigned Instagram accounts is empty?

Check professional-account eligibility, portfolio ownership, and the account selected during consent. If those checks pass, capture the configuration ID, attempt time and failure step for Meta support. Developer reports include unresolved empty responses, so do not invent an account ID or declare a successful connection.

## Related Adeli pages

- [Instagram API for Developers](https://www.tryadeli.com/product/instagram.md): Publish, schedule, and read analytics for Instagram through one API. Adeli owns the Instagram developer app, token refresh, and Meta review. Free to start.
- [Social Media Posting & Scheduling API](https://www.tryadeli.com/social-media-posting-api.md): Adeli's social media posting and scheduling API publishes to Instagram, TikTok, YouTube, Facebook, X, and Bluesky in one request, now or at a set time. Free to start.
- [Social Media Messaging API](https://www.tryadeli.com/social-media-messaging-api.md): Read Instagram, Facebook, and WhatsApp messages and reply to Instagram and Facebook DMs through the same API you publish with. No outbound or unsolicited messaging.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Nick Reyes is the CTO of Adeli. Previously he worked in security, cryptography, and authentication systems, with 10 years of experience at Google, Airbnb, and Phantom.
