An Instagram connection can return to your app with an authorization code, exchange that code successfully, and still be unable to create a media container.
OAuth completed. Publishing eligibility did not.
This checklist is for the failure before Meta starts fetching an image or processing a video. If your application already has a container ID, the account and permissions cleared the first gate. Use the container lifecycle in the Instagram Content Publishing API reference instead.
Prove where the request stopped
Do not start with the media file unless the request reached POST /{ig-id}/media.
Classify the last completed step first.
| Last completed step | What is already proven | Inspect next |
|---|---|---|
| Authorization window opened | App ID and authorization URL were accepted | User decision, redirect URI, and requested permissions |
Callback contains code | The user approved the request | Code age, one-time use, and token-exchange parameters |
| Token exchange returned an access token | The code and app credentials matched | Granted permissions, token type, and API host |
GET /me returned an identity | The token can read its account identity | account_type and production access level |
POST /{ig-id}/media returned a container ID | Account and publishing permission checks passed | Media fetch and container processing |
This split matters because changing a JPEG cannot fix an expired authorization code, a personal account, or a token without publishing permission.
Name the login configuration before reading the error
Meta supports two publishing configurations. Their permission names look similar, but the tokens and hosts are not interchangeable.
| Check | Instagram Login | Facebook Login for Business |
|---|---|---|
| User signs in with | Instagram credentials | Facebook credentials |
| Publishing token | Instagram User token | Facebook Page token |
| API host | graph.instagram.com | graph.facebook.com |
| Facebook Page | Not required | Required and linked to the Instagram account |
| Minimum publishing permissions | instagram_business_basic, instagram_business_content_publish | instagram_basic, instagram_content_publish, pages_read_engagement |
Meta documents this split in the Instagram Platform overview. Write the selected login type into the connection record. If the system has to infer it later from a token string or account ID, error handling will eventually send a valid token to the wrong host.
Read the OAuth result literally
For Business Login for Instagram, a successful callback contains a code.
That code is valid for one hour and can be used once. A canceled flow returns
error=access_denied and error_reason=user_denied instead.
Those are different outcomes. Do not turn a user denial into a token-exchange retry.
The exchange request must also use the same redirect_uri used to begin the
flow. Meta calls this out explicitly in its
Business Login for Instagram guide.
curl -X POST https://api.instagram.com/oauth/access_token \
-F "client_id=${INSTAGRAM_APP_ID}" \
-F "client_secret=${INSTAGRAM_APP_SECRET}" \
-F "grant_type=authorization_code" \
-F "redirect_uri=${OAUTH_REDIRECT_URI}" \
-F "code=${AUTHORIZATION_CODE}"Run this exchange on the server. The request contains the Instagram app secret. If Meta reports that the matching code was not found or was already used, do not retry the same code. Start a new authorization flow and preserve the exact provider error for diagnosis.
Check the permission list before storing the connection
The token response includes the permissions the user granted. For Instagram Login publishing, both of these must be present:
instagram_business_basicinstagram_business_content_publish
The older business_basic and business_content_publish scope names were
deprecated in January 2025. A consent screen that still requests the old names
can look correct while producing a token that cannot call current endpoints.
For Facebook Login, do not substitute the Instagram Login names. The required
publishing permission is instagram_content_publish, without the business_
segment.
An application-side gate makes this mismatch visible before a scheduled job tries to publish:
type InstagramConnectionCheck = {
accessLevel: "standard" | "advanced";
accountType: string;
apiHost: string;
grantedPermissions: string[];
loginType: "instagram" | "facebook";
servesExternalAccounts: boolean;
};
const requiredPermissions = {
instagram: ["instagram_business_basic", "instagram_business_content_publish"],
facebook: [
"instagram_basic",
"instagram_content_publish",
"pages_read_engagement",
],
} as const;
function diagnoseInstagramConnection(input: InstagramConnectionCheck) {
const issues: string[] = [];
const expectedHost =
input.loginType === "instagram"
? "graph.instagram.com"
: "graph.facebook.com";
if (!["BUSINESS", "CREATOR"].includes(input.accountType.toUpperCase())) {
issues.push("Connect an Instagram Business or Creator account.");
}
if (input.apiHost !== expectedHost) {
issues.push(`Use ${expectedHost} for the selected login type.`);
}
for (const permission of requiredPermissions[input.loginType]) {
if (!input.grantedPermissions.includes(permission)) {
issues.push(`Request ${permission} and reconnect the account.`);
}
}
if (input.servesExternalAccounts && input.accessLevel !== "advanced") {
issues.push("Obtain Advanced Access before connecting customer accounts.");
}
return issues;
}This check does not prove that Meta will accept a particular post. It proves that the stored connection matches the documented publishing configuration.
Ask the token which account it represents
After exchanging for a long-lived Instagram User token, query /me before
marking the connection ready.
curl \
"https://graph.instagram.com/${API_VERSION}/me?fields=id,username,account_type" \
-H "Authorization: Bearer ${INSTAGRAM_ACCESS_TOKEN}"The account must be a professional Business or Creator account. A consumer account is a stop condition, not a warning. Repeating the same request with the same account will not make it eligible.
Kavenio performs this identity check during connection. It normalizes Meta's
BUSINESS and CREATOR values and rejects other account types with the message
Instagram publishing requires a Business or Creator account. The connection
does not reach the posting queue.
A test account working does not prove customer access
Standard Access is enough for professional accounts owned or managed by people who have a role on the app. It is intended for development and testing.
Advanced Access is required when the app serves professional accounts it does not own or manage. Meta requires App Review and Business Verification for that level. The current boundary is documented in the access-level section of the platform overview.
This produces a common false conclusion:
- A developer connects the team's Business account.
/meworks and a test post publishes.- An unrelated customer cannot complete the same flow.
- The team starts changing OAuth code that already works.
The useful comparison is not “test versus production.” It is “account with an app role versus customer account without one.” If only the first group works, inspect access level and review status before changing the token exchange.
Facebook Login adds Page gates
Skip this section for direct Instagram Login. That configuration does not require a Facebook Page.
With Facebook Login for Business, the Instagram professional account must be
linked to a Page. The person granting access must be able to perform an
admin-equivalent Page task. Meta maps the Content and Full control tasks
to the ability to grant instagram_content_publish.
There is another Page-level stop condition: Page Publishing Authorization. Meta's Content Publishing guide states that a linked account cannot publish while required authorization is incomplete. It also says the API does not expose whether a Page currently requires that authorization. This is one of the few cases where the correct diagnostic step is a user-facing instruction to check Page settings, not another API request.
Use the symptom to choose the next check
| Symptom | Most useful check | Correct stop or fix |
|---|---|---|
Callback contains access_denied | User decision | Stop retrying; let the user start consent again |
| Token exchange says the code was used or not found | Code age and reuse | Start a new authorization flow |
| Token works on one Graph host but not the other | Stored login type | Match Instagram token to graph.instagram.com or Page token to graph.facebook.com |
/me returns a personal account | account_type | Connect a Business or Creator account |
| Team-owned accounts work but customer accounts do not | Access level | Complete App Review and Business Verification for Advanced Access |
| Publishing permission is absent | Granted permission list | Request the correct scope set and reconnect |
| Facebook Login cannot find a usable Instagram account | Page link and Page task | Link the professional account and grant Content or Full control |
| Page-linked publishing remains blocked | Page Publishing Authorization | Ask the Page owner to complete the required authorization |
What Kavenio marks as ready
Kavenio's current direct Instagram flow requests
instagram_business_basic and instagram_business_content_publish, exchanges
the short-lived token for a long-lived token, and reads the professional
identity from graph.instagram.com.
Its readiness result separates three pre-upload failures:
INSTAGRAM_ACCOUNT_NOT_FOUND: no Business or Creator account is available;INSTAGRAM_ACCOUNT_NOT_SELECTED: the connection has no publishing destination;INSTAGRAM_PERMISSION_MISSING: the account must reconnect with publishing permission.
Only a ready connection reaches the container sequence described in the Kavenio Instagram documentation. At that point, media hosting, format validation, container processing, and publishing limits become relevant. Before that point, they are distractions.
The current rule is simple: prove the login path, code exchange, permission set, professional identity, and access level in that order. Do not send media to a connection that has not passed those checks.