DEV Community

Cover image for Social media uploads without an API: an uploader that drives the browser
arti1984
arti1984

Posted on Edited on

Social media uploads without an API: an uploader that drives the browser

Every platform wants its own API app for automated uploads: a developer account, a review, scopes, tokens — and on TikTok an approval an individual practically never gets. I just wanted to publish one video to YouTube, Facebook, Instagram and TikTok. So I built the opposite: a tool that uses no API at all and drives the browser instead.

The idea: the browser is the interface

Instead of talking to an API, the tool opens the ordinary website, picks the file, types the title and the text and clicks "Post" — exactly what I would do with a mouse, just reproducible and from the command line.

That sounds primitive, but it has one solid advantage: it works wherever there is no upload API. And it needs no plaintext credentials, because it reuses a session that is already signed in.

Under the hood it is patchright and Playwright, so a remote-controlled Chromium or Firefox. The session lives either as a cookie file (cookies/….json) or as a persistent browser profile (profiles/<browser>/<account>) on your own disk.

What happens at runtime

The flow is intentionally boring and deterministic:

sau facebook upload-video --account creator --file video.mp4 --title "Title"
Enter fullscreen mode Exit fullscreen mode
  1. The CLI reads the arguments and picks the platform, the browser and the session.
  2. The uploader opens the home page, waits for the composer selectors and attaches the file to the input[type=file] — no file dialog, straight through the DOM.
  3. Title, description and hashtags are typed in.
  4. The publish button is found by its label (in several languages, because the UI is German, English or Chinese depending on the account).
  5. Afterwards the run writes its result as JSON: Instagram uses the media id from the final create response, Facebook waits for the exact success toast, and the Facebook Page flow opens the published list and looks the post up by content id.

No agent, no LLM, no decisions at runtime: one command, a fixed sequence of clicks. That is what makes debugging bearable — when something breaks, it is a selector, not the mood of a model.

Captions without an AI service

The tool can suggest descriptions and hashtags by itself (--auto-desc, --auto-tags, sau caption). That is deliberately rule-based: text from the title and the notes, one fixed call to action per platform, hashtags from the significant words, everything trimmed to the platform's character limits. Same input, same output, no network call, no cost.

That is enough for my use case. If you want genuinely creative copy, plug a model in there — the seam is clearly defined.

Getting started

uv venv
source .venv/bin/activate        # Windows: .venv\Scripts\Activate.ps1
uv pip install -e .
patchright install chromium
cp conf.example.py conf.py
sau --help
Enter fullscreen mode Exit fullscreen mode

Then sign in once — the window opens visibly, and you solve captchas and 2FA yourself:

sau youtube login --account creator
sau youtube check --account creator
sau youtube upload-video --account creator --file videos/demo.mp4 --title "Title" --visibility public
Enter fullscreen mode Exit fullscreen mode

--account is only a local alias for a stored session. Facebook, Facebook Pages and Instagram have the same three steps (login, check, upload-video or upload-note). TikTok has no CLI command but its own Python uploader, which can take over an already running browser over CDP.

Where it hurts

Honesty belongs in an article like this:

  • Selectors break. When a platform redesigns its web UI, the tool no longer finds the button. Then it stops and gets adjusted — it does not guess.
  • The terms of service. Cookie automation is not something most platforms provide for. Risk control, captchas and, in the worst case, bans are real risks. If you run this in production, you should know what you are doing.
  • "Unknown" is not "failed". The result file is written with submission_unknown before the submit click, so a crash leaves a record and nothing is re-sent automatically. A silent composer proves nothing either way: sau instagram verify and sau facebook verify reopen the live post and check id, author and caption — the check I trust more than any toast.
  • Scheduling is the platform's job. Facebook Pages schedule through the page's own scheduling dialog; Instagram and Facebook profiles cannot schedule at all.

Conclusion

For posting regularly to a handful of your own accounts without wiring up four API integrations, browser automation gets you surprisingly far. It is not a product for client accounts, it is a tool for your own account — with all the rough edges that come with it.

Code, handbook and every flag: github.com/arti1984-collab/social-auto-upload-international (MIT).

Top comments (3)

Collapse
 
autenai profile image
Auten •

How do you handle the "unknown" case today: does the uploader open the profile feed and look for the new post before it reports, or is that left to the person?

We post to Instagram from automation too (I'm on the Auten team), and opening the live post afterwards became the only check we rely on, since what the composer showed and what the feed showed were not always the same.

Collapse
 
arti1984collab profile image
arti1984 •

Fair point — and it is the one place where my article was too generous.

By default the upload command trusts the composer's own signal, not the feed. The Facebook profile path polls up to 90 seconds for the exact success toast and fails with "never saw an explicit success state" otherwise; if a toast was already on screen before the click, it refuses to submit rather than attribute someone else's post to this run. Instagram installs a listener on the create endpoint before clicking, treats a 202 as processing, keeps waiting, and only records published with media id and shortcode from the final payload. YouTube reads the youtu.be link out of the Studio dialog. TikTok raises "did not confirm publishing; check the composer manually".

The exception is the Facebook Page flow, which does what you describe: it captures the story-creation mutation, then opens the published (or scheduled) post list for that Page and matches the post by content id and title.

What I would defend is how "unknown" is handled. The result file is written with submission_unknown before the submit click, so a crash or a window closed mid-flight leaves a record on disk and nothing is ever resent automatically. The feed visit is a separate, explicit step: sau instagram verify --post-url … --expected-text … reopens the live post and checks og:url, the owner id from instapp:owner_user_id, and that the caption appears both in main and in the meta description, so a passing comment cannot make the check green. The Facebook verify matches post id, author link and text on the profile.

Curious how you handle the gap in the other direction: after opening the live post, do you check anything beyond caption and author — for example that the media really finished processing?

Collapse
 
autenai profile image
Auten •

Writing submission_unknown before the click is the part I'd copy. On our side the API's container status covers processing, so the live check is about how the post renders: once the feed showed a 4:5 image cropped to square, top and bottom cut, while the edit screen looked fine. Since then we check the image as the feed displays it, not only caption and author.

Does verify look at the og:image dimensions too, or is crop out of scope for sau?