API Documentation

Publish to Instagram from your automations.

Rubinyun is a simple API: you queue a piece of content and it publishes at the chosen time (or at the best time read from your data), even with your computer off. Perfect to call from n8n, Make, a script or your agent.

AuthenticationSchedule a postBest time QueueResultsTokens and limitsMedia checksUpload mediaChannelsn8n template Instagram from n8nFAQComparison

Authentication & basics

All requests go to a single endpoint, with the action in the parameter action:

https://www.chrononyte.com/rubinyun/api.php

Authentication with API key + secret (find them in your area → API Keys). The recommended way is to send them as headers: that way they never end up in URLs or server logs.

# recommended: keys in headers
X-WS-Key: rby_live_...
X-WS-Secret: rby_sk_...

Prefer a single header? Authorization: Key <key>:<secret> works too, and it is what tools with a built-in credential expect: in n8n it maps onto a Header Auth credential, so the keys live in the credential instead of sitting in the workflow.

# one header, for tools that store credentials for you
curl "https://www.chrononyte.com/rubinyun/api.php?action=list"   -H "Authorization: Key rby_live_…:rby_sk_…"

Alternatively (handy for quick tests) you can pass them as query parameters. It works, but the keys stay in the URL: use headers when you can.

# alternative: keys in query (they stay in the URL)
?api_key=rby_live_...&api_secret=rby_sk_...

Response is always JSON. Success: {"ok":true, ...}. Error: {"ok":false,"code":"insufficient_tokens","error":"..."}.

Error messages are always in English, whatever language your interface is in. Next to them sits code, a stable code that never changes: branch on that, not on the text.

Schedule a post POST

action=add: queues a piece of content.

ParameterDescription
typeimage · carousel · reel · story (default image)
scheduled_at req.date and time YYYY-MM-DD HH:MM (your account's timezone)
image_url / video_urlpublic media URL (for image/reel/story)
media_urlsfor a carousel: multiple URLs separated by newline or comma (2–10)
captionpost text (ignored for stories)
channelid of the channel to publish on (empty = Main)
# schedule an image for December 1 at 09:00
curl -X POST "https://www.chrononyte.com/rubinyun/api.php?action=add&api_key=rby_live_...&api_secret=rby_sk_..." \
  -d type=image \
  -d image_url="https://yoursite.com/photo.jpg" \
  -d caption="Hello from my workflow" \
  -d scheduled_at="2026-12-01 09:00"

Best time GET

action=besttime: the time recommended by your account's data. Parameter when: today · tomorrow · YYYY-MM-DD.

curl "https://www.chrononyte.com/rubinyun/api.php?action=besttime&when=tomorrow&api_key=...&api_secret=..."
# -> {"ok":true,"slot":{"at":"2026-08-02 18:00","hour":18,"weekday":"Sunday","day_full":false,
#      "why":"engagement by weekday and hour (your own posts)","why_code":"history","sample":42,
#      "busy_skipped":0,"crowded":false}}

keep_day=1 · the day is yours, the hour is ours

Without it, if the best hour of the day you asked for has already passed, the answer rolls over to the next day: handy when you just want the soonest good slot, wrong when the day was your decision. With keep_day=1 the day is kept and only the hour moves, to the best slot still ahead on that day.

If no good slot is left on that day, you get "day_full":true with at:null: nothing is scheduled behind your back on a day you didn't choose. Branch on day_full and either pick another day or set the hour yourself.

curl "…api.php?action=besttime&when=2026-07-17&keep_day=1&api_key=…&api_secret=…"
# -> {"ok":true,"slot":{"at":"2026-07-17 18:00","hour":18,"weekday":"Friday","day_full":false, …}}
# giorno pieno: {"ok":true,"slot":{"at":null,"hour":null,"day_full":true, …}}

busy_skipped · it steps around what you have already scheduled

The recommendation reads the queue of that channel. Your best hour comes from your publishing history, which does not change between one call and the next: five calls in a row would hand you the same minute five times, and five posts would land on top of each other. So a slot closer than two hours to something already on that channel is skipped and you get the next best one, on the same day if there is room, otherwise on the day after. busy_skipped counts how many were passed over.

Only the suggestion moves. add still takes any scheduled_at you give it, so two posts at the same hour stay allowed if that is what you want. Other channels do not count: two different profiles going out at the same time are not competing. If a queue is packed for two weeks running you still get an answer, marked "crowded":true.

Typical flow: call besttime, take slot.at and pass it as scheduled_at to add. No date of your own? call it without keep_day and you get the soonest good moment: day and hour.

Managing the queue

GET action=list

List of queued posts (ordered by time).

POST action=update · delete · retry

With id=<post>: edit caption/time (only if still scheduled), remove, or re-schedule a failed one.

POST action=runmine

Immediately publishes the due items in your queue (normally the engine does this on its own).

Post results GET

action=insights with id=<post>: likes, comments, reach and the link to the post on Instagram. It closes the loop: you publish from your automation and pull the numbers back in.

curl "https://www.chrononyte.com/rubinyun/api.php?action=insights&id=p_20260713_101500_ab12cd&api_key=...&api_secret=..."
# -> {"ok":true,"likes":42,"comments":7,"reach":980,"permalink":"https://www.instagram.com/p/..."}

Check keys and balance GET

action=ping: checks that your keys work and returns your balance (purchased tokens + this month's free ones). It costs nothing.

curl "https://www.chrononyte.com/rubinyun/api.php?action=ping&api_key=...&api_secret=..."
# -> {"ok":true,"email":"tu@esempio.com","billing":{"tokens":25,"free_remaining":7,"sub_remaining":150,"unlimited":false}}
# tokens = posts bought in a pack: they never expire.
# free_remaining = posts left in this calendar month (free plan).
# sub_remaining = posts left in the current subscription billing period.
# Available to publish right now = the three added together.

Tokens, limits and errors

1 token = 1 post, whether it's a single photo or a ten-slide carousel. Every month you get 10 free posts (email confirmation required); beyond that you need purchased tokens, which never expire. A token is charged only on a successful publish: if Instagram rejects it, you don't pay.

Your balance can also run out between scheduling and publishing, and in that case add already said yes. The post is not published: it turns failed with out of tokens, an email goes to the account and a notice appears in the console. Nothing is charged and nothing retries by itself: top up, then call action=retry on that post, or watch for it in action=list.

A refusal comes back with an HTTP status that says so (the number next to each code below), never with a 200. That way a client that only looks at the status still notices: curl -f, the n8n HTTP node, raise_for_status. The body does not change, so branching on code works exactly as before, and it stays the better way: the status groups errors, the code names them.

codeWhat it means
unauthorized (401)missing or wrong keys.
insufficient_tokens (402)balance used up (posts already queued count too): top up or remove a scheduled post. The response also carries available and pending.
email_not_verified (403)the 10 free posts unlock once you confirm your email. Purchased tokens work regardless.
channel_not_connected (409)the channel has no Meta token / IG account ID linked yet.
channel_not_found (404)your keys are fine: it is the channel id that does not exist. List them with action=channels. The id you sent comes back in value.
channel_mismatch (403)you used a channel's own keys and asked for a different channel. Drop the channel parameter, or use the keys of the channel you mean: a channel's keys never reach another channel.
invalid_type · invalid_date · missing_scheduled_at · no_media · carousel_needs_2 · carousel_max_10 · reel_needs_video (400)invalid parameter: the code tells you what's missing (type, date, media URL).

Tip: call ping before a long loop and stop if the balance is too low, instead of finding out halfway through.

Media checks (warnings, not walls)

When you queue a post, Rubinyun reads the media (duration, size, dimensions, codec, container) and tells you what looks off, in a warnings array on the response. Nothing is blocked: Instagram in practice accepts a good deal more than it documents, and refusing a file that would have worked is the worse mistake.

# -> {"ok":true,"post":{ … ,"warnings":["not_9_16","has_edit_list"],
#      "probe":{"duration":5.06,"w":960,"h":540,"codec":"h264","size":1132000}}}

Warning codes are stable, like error codes: video_under_3s, video_over_15min, video_over_60s_story, not_9_16, moov_not_first, image_not_jpeg, media_unreachable… The probe object is what we actually read from your file, so when Instagram rejects something you can compare the two.

Instagram's own rules, for reference: reels 3s to 15min, mp4/mov, H.264 or HEVC, up to 300MB, 9:16 recommended; stories up to 60s; images up to 8MB.

Upload media POST

Instagram does not take a file: it downloads the media from a public URL. If you already have one (your site, S3, a CDN) just pass it. If you do not, upload.php gives you one: send the file, get back the URL to use as image_url.

curl -X POST "https://www.chrononyte.com/rubinyun/upload.php" \
  -H "X-WS-Key: rby_live_…" -H "X-WS-Secret: rby_sk_…" \
  -F image=@"photo.jpg"

# -> {"ok":true,"url":"https://www.chrononyte.com/rubinyun/uploads/acc_…/m_….jpg",
#     "parking":{"used_bytes":3133662,"limit_bytes":2147483648}}

Same keys, same headers. Formats: jpg, png, mp4, mov (the field is called image for videos too). The file is deleted from the server the moment it is published: your media does not stay with us. If it never gets published it expires on its own after 14 days, unless a post still needs it (scheduled, processing, or failed and waiting for a retry). A channel's own keys work here too, and the file lands in that channel's folder.

This is parking, not cloud storage: one pool per account, and it is the total you can have parked at once, not a monthly allowance. It does not renew: it simply frees up as your posts go out. How much you get depends on the plan: 200 MB on Free and token packs, 2 GB with the €9 subscription, 5 GB with Unlimited. Per file: images up to 8 MB (Instagram's own limit), videos up to 100 MB. Every upload answers with parking.used_bytes and parking.limit_bytes so an automation can check where it stands; when there is no room left the error is parking_full, and file_too_big / content_mismatch cover the rest.

Multiple channels (profiles) GET

An account can have several Instagram profiles, each with its own queue. action=channels lists them (id + name). From there you have two equivalent ways in: pass channel=<id> to the other calls using your main keys, or use that channel's own key pair (every channel gets one: handy when you hand access to a client or to a separate workflow). Tokens stay a single balance, shared by the account.

curl "https://www.chrononyte.com/rubinyun/api.php?action=channels&api_key=...&api_secret=..."

n8n template: ready in 2 minutes

The missing node to publish to Instagram from your automations. Download the workflow, import it into n8n, paste your keys and you're online.

Download the n8n workflow
  1. In n8n: Workflows → Import from File and choose the downloaded JSON.
  2. Create a Header Auth credential: name Authorization, value Key YOUR_KEY:YOUR_SECRET, and select it in the HTTP nodes. The keys stay in the credential, not in the workflow you might share.
  3. Under Body Parameters set image_url, caption and scheduled_at.
  4. Run it. Rubinyun publishes at the chosen time, even with your PC off.

The template also carries a spare node, off and wired to nothing: Optional: upload a media file. Turn it on only if you have a file and no public URL for it: feed it the file as binary data and it hands back the url to use as image_url.

It works the same way with Make, Zapier (HTTP module) or any HTTP client.