API Documentation

Publish to Instagram, Facebook and Threads 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 mediaChannelsClient approvalsWebhooksn8n nodeMCP servern8n 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 · text (default image). text is a text-only post, Threads channels only (in testing): no media, the caption is the post, 500 characters at most; on the other channels it is refused with text_needs_threads.
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).

Every post carries its channel: channel_id, channel_name, platform and profile_name (the connected Page or Instagram account). With channel=all and your account keys you get the queues of all your channels merged in one list, ordered by time; the same parameter on runmine runs every connected queue. Everything else (update, delete, retry) works on one channel: pass the channel_id of the post.

curl "https://www.chrononyte.com/rubinyun/api.php?action=list&channel=all&api_key=...&api_secret=..."
# -> {"ok":true,"channel":"all","posts":[{"id":"p_…","channel_id":"ch_…","channel_name":"Purpure Punch","platform":"facebook","profile_name":"Samantha Riva", …}, …]}

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.
empty_body (400)the POST arrived without a body. Almost always you called https://chrononyte.com without www: the redirect to www.chrononyte.com drops the body (and the Authorization header). Always call https://www.chrononyte.com/rubinyun/.
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

Each channel has a platform: instagram, facebook or threads (Threads is built and in testing on real accounts; it is connected from the console, with your own Meta app). A post is scheduled on one channel, and the channel decides where it goes out.

An account can have several channels, Instagram profiles and Facebook Pages, 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=..."

Client approvals POST

A scheduled post can be sent to a client for review. action=review_send with id=<post> returns a review link: the client opens it without any Rubinyun account, sees the post as it is (media, caption, channel, date) and approves or rejects it, with an optional comment. Free, no cap.

curl -X POST "https://www.chrononyte.com/rubinyun/api.php?action=review_send"   -H "Authorization: Key rby_live_…:rby_sk_…"   -d "id=p_20260914_185700_ab12cd"
# -> {"ok":true,"review_url":"https://www.chrononyte.com/rubinyun/review.php?t=rv_…","post":{…,"approval":{"status":"pending",…}}}

While approval.status is pending or rejected the post does not go out, even past its time: it leaves as soon as it is approved. A rejected post stays in the queue with the client's comment in approval.comment. If you update a reviewed post it goes back to pending with the same link. action=review_cancel removes the review and frees the post. list carries the approval field on every post that has one.

Webhooks: Rubinyun calls you

In your area (Console → Webhook) register an https address and Rubinyun sends it a signed POST on every event: post.scheduled, post.published, post.failed, post.review_sent, post.approved, post.rejected (plus test, from the console button). No retries: the last 20 deliveries, with the HTTP status your server returned, are listed in the console.

POST https://your-server/hook
Content-Type: application/json
X-Rubinyun-Event: post.published
X-Rubinyun-Delivery: whd_…
X-Rubinyun-Timestamp: 1789480800
X-Rubinyun-Signature: t=1789480800,v1=<hex>

{"event":"post.published","at":"2026-09-14T18:57:00+02:00","delivery":"whd_…","account":"acc_…",
 "data":{"id":"p_…","channel":"ch_…","type":"image","status":"published","scheduled_at":"2026-09-14 18:57",
         "caption":"…","media":[{"url":"…","kind":"image"}],"media_id":"1784…","error":null,"review":null}}

Verifying the signature: v1 is the HMAC-SHA256, keyed with your secret, of the string <timestamp>.<raw body>. Recompute it, compare with a constant-time function, and refuse timestamps older than a few minutes.

# Node
const [t, v1] = sig.split(',').map(s => s.split('=')[1]);
const mac = crypto.createHmac('sha256', SECRET).update(t + '.' + rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
# PHP
$ok = hash_equals(hash_hmac('sha256', "$t.$raw", $secret), $v1);

n8n node: the operations as a node, on self-hosted n8n

There is a community node, n8n-nodes-rubinyun, that wraps the whole API: Post (create, update, delete, retry, list), Channel, Best Time, Queue, Insight and Account, with a Rubinyun API credential that holds the key and the secret. No HTTP node to fill in. It installs on self-hosted n8n; on n8n Cloud, community nodes appear only after n8n has verified them, and this one is not verified yet.

  1. In your self-hosted n8n: Settings → Community Nodes → Install, package name n8n-nodes-rubinyun.
  2. Create a Rubinyun API credential with your key and secret: the test button calls the API and tells you whether the pair is accepted.
  3. Chain Best Time → Post Create: in Post Create set Scheduled At to {{ $json.slot.at }}, the value Best Time returns.

Refusals come out with the API's stable code in front of the message, so a workflow can branch on it. Source, README and version history: github.com/chrononyte/n8n-nodes-rubinyun, package on npm. A three-minute walkthrough, from install to the AI Agent tool: watch it on YouTube.

MCP server: Claude Code and Cursor talk to Rubinyun

Rubinyun hosts an MCP server (Model Context Protocol) at https://www.chrononyte.com/rubinyun/mcp.php. Point Claude Code, Cursor or any client that speaks MCP over HTTP at that address with your key and secret in the headers, and the assistant gets the whole API as tools: account, channels, list posts, best time, schedule, update, delete, retry, run the queue, post results, send for review and cancel review. Nothing to install, nothing to host, and the API itself does not change: the server calls it for you, with the same keys and the same rules (tokens, approvals, stable error codes).

# Claude Code
claude mcp add --transport http rubinyun https://www.chrononyte.com/rubinyun/mcp.php \
  --header "X-WS-Key: rby_live_..." --header "X-WS-Secret: rby_sk_..."
# Cursor: .cursor/mcp.json (or the global one)
{
  "mcpServers": {
    "rubinyun": {
      "url": "https://www.chrononyte.com/rubinyun/mcp.php",
      "headers": { "X-WS-Key": "rby_live_...", "X-WS-Secret": "rby_sk_..." }
    }
  }
}

With the account keys the tools see every channel (rubinyun_list_posts and rubinyun_run_queue default to all of them); with the keys of one channel they act on that channel only. A refusal comes back inside the tool result with isError and the same stable code as the API, so the assistant can read it and correct itself. The transport is JSON-RPC over POST, one request per call, no event stream and no session: it fits any client that supports remote MCP servers with custom headers.

The connectors of claude.ai on the web want OAuth rather than headers: there, this server does not plug in yet. A local package for those clients is on the list.

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.