Skip to content

omnipost/instagram

Omnipost Instagram: the account's feed, the publication of pictures, carousels and reels, and the long-lived token's refresh, through the Instagram API with Instagram Login.

Floor
Provider · Omnipost
Version
1.x
Conformance suite
not wired
Verified for real
Its README does not say
Licence
MIT
composer require omnipost/instagram:1.x-dev

Instagram for omnisense/omnipost: the account and its feed read, pictures, carousels, reels and stories published, the long-lived token refreshed and the account connected from a back office, through the Instagram API with Instagram Login - no Facebook Page needed. The provider calls, on https://graph.instagram.com/v23.0 (Api::BASE_URI, Api::VERSION, the base_uri and api_version options):

Operation Endpoint
a container POST /<user>/media
its state GET /<id>?fields=status_code,status
publishing it POST /<user>/media_publish (creation_id)
a media GET /<id>?fields=id,permalink,timestamp,media_type
the account GET /<user>?fields=id,username,name,profile_picture_url,followers_count,media_count,account_type
the feed GET /<user>/media?fields=...&limit=&after=
refresh GET https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token (unversioned)
connect https://www.instagram.com/oauth/authorize, POST https://api.instagram.com/oauth/access_token, GET https://graph.instagram.com/access_token?grant_type=ig_exchange_token

<user> is the user_id option, me by default. The token goes as the access_token parameter of every Graph call. There is no sandbox: an app in development mode and a tester account are the test setup.

Installation

composer require omnisense/omnipost omnipost/instagram

Requires PHP 8.2, omnisense/omnipost (1.x) and symfony/http-client (6.4, 7 or 8). In a Symfony application with Omnipost\Bridge\Symfony\OmnipostBundle, the factory is registered as soon as the package is installed.

Credentials

From the README:

  • A professional Instagram account (Business or Creator).
  • A Meta app (developers.facebook.com, type Business) with the Instagram API with Instagram Login product, the permissions instagram_business_basic and instagram_business_content_publish, the site's redirect URI declared: its app id and app secret.
  • While the app is in development mode, the account added as an Instagram Tester and the invitation accepted. A site that posts on its own account needs no App Review; App Review comes in to serve accounts that are not the app's testers.
  • A token: generated in the app's dashboard for the tester's account, or obtained with authorizationUrl() and exchange(). It lives 60 days; refresh() gives 60 more to a token at least 24 hours old and not dead yet.
  • The media at a public URL Meta's servers can fetch (a signed, temporary URL will do).

Options

Option Type Default Required
access_token string none for every call but connecting the long-lived token
app_id string none for authorizationUrl(), exchange()
app_secret string none for exchange()
user_id string me no the account the calls are for
share_to_feed bool true no a reel also on the profile's grid
api_version string v23.0 no
base_uri string https://graph.instagram.com no

No option is required to build the provider (null and '' are as if not given). Post options read (Post::$options, or Instagram's variant's): share_to_feed, thumb_offset.

Capabilities

Call Supported
publish() REEL yes: one video, 9:16, 3 s to 15 min, MP4 or MOV, at most 1 GiB
publish() IMAGE yes: one JPEG, 9:16, 4:5 or 1:1, at most 8 MiB, with its alt
publish() CAROUSEL yes: 2 to 10 pictures or videos
publish() STORY yes: one picture or video, no caption sent
publish() VIDEO no: refused by the validator (a video goes out as a reel)
caption, hashtags 2200 characters with the hashtags, 30 hashtags; width at least 320 px
scheduling no: a scheduled post is refused
status() yes: publishes the container when it is ready
delete() no: NotSupportedException (the API cannot)
account(), feed() yes
refresh(), token() yes
authorizationUrl(), exchange() yes

Behaviour

Publishing in three steps. publish() creates a container and answers PENDING with its id; status(container) answers PROCESSING while Instagram works, FAILED on ERROR or EXPIRED, and on FINISHED publishes it and answers PUBLISHED with the media's id and its permalink. status() also takes a media's id. Two workers must not poll one container at once (it would be published twice): hold a lock around status(). The container's fields per kind: Publishing.

Validation. One set of capabilities for every kind (the 10 media are the carousel's, 3 s - 15 min and the sizes the reel's), plus what depends on the kind: one media unless a carousel (at least 2 for one), a reel is a 9:16 video, a picture post a picture. The format and size checks run only when the media carries a mime and a size.

Feed. limit between 1 and 100; next is the after cursor only when Instagram announces a next page. A VIDEO with media_product_type REELS (or none) is a REEL, another VIDEO; CAROUSEL_ALBUM a CAROUSEL with its children; a STORY product a STORY; anything else an IMAGE. A picture's thumbnail is the picture, a carousel's its first child's.

Tokens. refresh() and exchange() answer a Token with its end (expires_in) and the provider uses the new token from then on; token() is the token held, its end unknown unless it came from one of them. exchange() strips the #_ Instagram appends to the code, and gives the permissions granted as scopes and the account's id as accountId.

Messages. Every access_token=, client_secret= and code= in an error message is masked, and a transport error is not chained to the client's exception, whose message carries the URL.

Example

<?php

require __DIR__.'/vendor/autoload.php';

use Omnipost\Instagram\InstagramProviderFactory;
use Omnipost\Model\Media;
use Omnipost\Model\Post;
use Omnipost\Platform;
use Symfony\Component\HttpClient\HttpClient;

$instagram = (new InstagramProviderFactory(HttpClient::create()))->create(['access_token' => getenv('INSTAGRAM_ACCESS_TOKEN')]);

$post = Post::reel(Media::video('https://site.example/media/reel.mp4', null, 42.0, 1080, 1920), 'Strauss, tonight.', null, ['violin']);
$publication = $instagram->publish($post->for(Platform::INSTAGRAM));   // PENDING, the container's id
while (!$publication->state->isFinal()) {
    sleep(5);
    $publication = $instagram->status($publication->id);               // PROCESSING, then PUBLISHED with the media's id
}
echo $publication->permalink, "\n";

foreach ($instagram->feed(null, 6)->items as $item) {
    echo $item->kind->value, ' ', $item->permalink, "\n";
}

The same flow was run on 2026-10-26 on a MockHttpClient (requests and answers in Publishing and Reading); Instagram itself was not called. In Symfony:

omnipost:
    providers:
        instagram:
            factory: instagram
            options:
                access_token: '%env(default::INSTAGRAM_ACCESS_TOKEN)%'
                app_id: '%env(default::INSTAGRAM_APP_ID)%'
                app_secret: '%env(default::INSTAGRAM_APP_SECRET)%'

Errors

Answer Exception
HTTP 401, Graph code 190, subcode 463 or 467 UnauthorizedException: the account is connected again
Graph code 4, 17, 32, 613 (call limits) ProviderException (Too many calls, try again later: ..., providerCode the code)
any other error (HTTP 400 or more, or an error / error_type in the body) ProviderException with the status and Graph's code
transport error ProviderException (Instagram could not be reached: ..., the token masked)
no token in an OAuth answer ProviderException
no access_token, app_id or app_secret for the call InvalidConfigException naming it
a post the network would refuse InvalidPostException, nothing sent
delete() NotSupportedException

Instagram caps the publications made through the API per account and per 24 hours; the provider does not read the cap (GET /<user>/content_publishing_limit).

Verified, and not

The package's tests (Tests/InstagramProviderTest.php, 12 tests) pass (2026-10-26, PHP 8.4) on answers written in the shapes Instagram documents - not recorded from Instagram. No publication or read was run against Instagram for this package (neither the README nor the history states one).

Responsibility

This package is an independent, unofficial client, not affiliated with, endorsed by or sponsored by Meta Platforms, Inc. (Instagram). Whoever runs it complies with the Meta Platform Terms; see the README's Responsibility section.

Propose a change docs/index.md