M

How to create a US Instagram automatic posting service for dummies

8 min readView source ↗

Cover image

Following my recent update of personal ad-hoc self-made services I wanted to share how I built an automatic posting service for Instagram. Against everything I expected, this has been the trickiest service I've had to build so far, however, with some good practices and a little bit of patience I managed to set it and make it work seamlessly.

I already had a pipeline that could generate short-form content: select a source video, assemble the final clip, add music, create the caption, and leave a package ready to publish. The last step still depended on an external posting bridge. It worked, until depending on it stopped making sense as it hurt my pocket knowing I could do it for free.

If the content is generated by my system, if the caption is produced by my pipeline, and if the Instagram account is mine, the final publishing step should also be under my control.

The real problem was not publishing. It was publishing without losing control

Publishing to Instagram through the API is not the same as “uploading a file”.

That is the first trap.

Instagram does not accept a local video file directly through the standard publishing endpoint. The media has to be available through a public URL that Meta can fetch. From there, the flow is very specific: create a media container, wait for Instagram to process it if it is a video, and then publish that container once it is ready.

On paper, this sounds simple. In practice, there are several edges that need to be handled properly.

You need a professional Instagram account. You need a Meta developer app. You need publishing permissions. You need an OAuth flow that gives you a valid token. You need to store that token without leaking it into logs. You need to refresh it before it expires. And you need to make sure your automation does not accidentally publish an old video from a previous run.

That is why I did not build just a loose script. I built a small service.

The core piece: a local service with its own contract

The project became a small Node.js service that wraps the uncomfortable parts of the Instagram Graph API and exposes them in two ways: HTTP and CLI.

Through HTTP, the basic contract is a POST /v1/instagram/publish request with the content type, the public media URL, the caption, and some publishing options.

Through the CLI, it becomes easier to integrate into automations:

The important part is not that the command is short. The important part is what happens behind it.

The service reads the configuration, validates the token, creates the Instagram media container, waits until the video container reaches a ready state, publishes it, stores a local job record, and returns a JSON response with the final status and the Instagram media ID.

That local job record matters more than it may seem.

When you automate publishing, you need to answer very basic questions: what did the system try to publish, when did it happen, which caption was used, where did it fail, and did Instagram actually publish it?

Without your own record, you end up depending on someone else’s dashboard.

The second bottleneck: OAuth

The most frustrating part was not writing the publisher itself. It was making Meta, Instagram, and OAuth fit together without turning the setup into a ritual.

First, the Instagram account had to be professional. Then it had to be correctly connected in Meta’s developer environment. After that came the developer app, the permissions, the client ID, the client secret, the redirect URI, and the authorization code exchange.

This is where a boring truth appears: an automation is only as stable as its authentication process.

So the service does not rely on “paste this token somewhere and hope it keeps working”. It has commands to generate the authorization URL, exchange the code, convert the token into a long-lived token, refresh it, and verify that it still works.

The difference between having these commands and not having them is huge.

Without them, every token expiration becomes a manual debugging session. With them, the process becomes explicit, repeatable, and documented.

Local video is not enough: Instagram needs a public URL

The content pipeline produces the video locally. Instagram needs a public URL.

That tension forces you to introduce an intermediate step, but not an intermediate posting tool. Just public storage.

The final video is uploaded to durable public storage, and that URL is passed to the local Instagram service. This distinction matters. The storage layer does not decide what gets published. It does not connect social accounts. It does not interpret captions. It only hosts the file so Meta can read it.

The publishing control stays inside my own service.

The final flow looks like this:

Generate the video.

Write a manifest with the hook, source media, music, paths, and duration.

Write the caption.

Upload the final MP4 to a public URL.

Verify the Instagram token.

Publish the Reel through the local service.

Store local delivery metadata.

The order matters. It prevents two common mistakes: publishing stale files and reporting success when only an intermediate step has completed.

Removing the third-party bridge completely

One of the most useful parts of the process was discovering that old paths to the third-party posting bridge still existed in the system.

It was not enough to say “from now on, use the local service”. The old route had to become impossible to use by accident.

That meant changing the pipeline contract. The old bridge delivery mode was removed from this workflow. Historical bridge metadata was kept only for auditing old output folders, not as a valid publishing path. The automation prompt was rewritten so that the final delivery must go through the local Instagram Graph API service. The project rules were also updated to explicitly reject third-party posting bridges for this workflow.

This may sound excessive until you have seen an automation follow an old instruction because it still exists somewhere in the project.

In automated systems, preferences are not wishes. They are contracts.

If a path must never be used again, it should fail at execution time. It should not merely be “discouraged” in a note.

Why this fits automated content better

The biggest benefit is not saving money on another tool. The biggest benefit is reducing the distance between generation and publishing.

When the pipeline creates a video, it already knows the hook, the music track, the generated file, the caption, the source media, and whether that source has already been used. If publishing is delegated to an external platform, part of that context is lost or duplicated.

When publishing happens inside my own flow, I can apply consistency rules before touching Instagram.

For example:

Do not reuse source videos unless explicitly allowed.

Do not reuse hooks unless explicitly allowed.

Do not publish if the render failed.

Do not send an old caption from a previous run.

Do not treat an ambiguous intermediate state as success.

Do not mix artifacts from different output folders.

In other words, the posting service is not an isolated tool. It is the last link in the production chain.

The result

Go through the following checklist

✅ Turn your Instagram account into a professional one

✅ Create a Facebook page

✅Link your Instagram account to your Facebook page

✅Create a meta developer account

✅Make a posting app inside your dev meta account

✅Follow the above instructions to activate the keys for your page and IG account and link them to your script

The flow is now much cleaner.

The content pipeline generates the video. The final file is uploaded to public storage. The local service publishes the Reel through the Instagram Graph API. The job is recorded. The third-party posting bridge is no longer part of the path.

It is not a huge infrastructure project, yet is a deliberately small MVP with the pieces that matter: OAuth, token refresh, image and Reel publishing, video container polling, local job storage, a CLI for automation, and a clear boundary between “file generated”, “file publicly available”, and “post published”.

What I find most interesting is that building your own posting service is not really about writing more code, it is about removing friction from your processes.

Prompts

npm run auth-url npm run exchange-code -- --code CODE --long-lived --write-env npm run refresh-token npm run verify-token
npm run publish -- --kind reel \   --media-url https://cdn.example.com/reel.mp4 \   --caption-file caption.txt \   --share-to-feed true

Related articles