API for websites
Any registered user can create a page and post to it from a website: a new review on your site becomes a duel or a poll here, under your page's name.
The API is plain HTTPS with JSON. It only publishes posts and reads your own posts and their results: votes and comments come from people, never through the API.
Quick start
1. Sign up and create a page: your profile, Create a page. Give it a handle, a short description and your website.
2. On the page, in API keys, create a key. Copy it right away and keep it on your server, never in browser code.
3. Optional but worth it: verify your domain on the page. You get a checkmark, higher limits, posts without review and links to your articles.
4. Check that the key works:
curl https://rupor.me/api/v1/me -H "Authorization: Bearer $RUPOR_KEY"
Keys
Only the page owner creates and revokes keys. A page can have up to 5 active keys, for example one per site or script.
post and read: can create, edit and delete posts and upload pictures. read only: can only read posts and results, for a stats widget on your site.
A key is shown once. We keep only its fingerprint, so a lost key cannot be recovered: revoke it and create a new one. A revoked key stops working on the next request.
Send it in the Authorization header: Authorization: Bearer rup_live_... A key in the URL is refused, because addresses end up in logs. Browsers cannot call the API from another site: it is for servers.
Domain verification
Optional. On the page, under Verify domain, press Get verification code, put the code on your site in one of two ways, then press Check.
A TXT record in the DNS of the domain (without www). Changes can take a few hours to spread:
Or a meta tag in the <head> of the home page (https://yoursite.com/):
After verification the page gets a checkmark, posts skip review, limits go up and source_url starts working. If you change the page's website to another domain, the checkmark is removed.
yoursite.com TXT "rupor-verification=<code from your page>"
<meta name="rupor-verification" content="<code from your page>">
Methods
All addresses start with /api/v1. Answers are JSON.
GET /meyour page, this key's permissions, limits and whether new posts go to reviewGET /typespost types and the rules for their fields, machine-readableGET /tagstags you can usePOST /mediaupload a picture, get a token for images or option_imagesPOST /postscreate a post (Idempotency-Key header required)GET /postsyour page's posts, newest firstGET /posts/{id}one of your posts with vote resultsPATCH /posts/{id}edit within 15 minutes, before the first vote or commentDELETE /posts/{id}delete one of your postsGET /openapi.jsonmachine description for client generators (no key needed)Post fields
POST /posts takes one JSON object. Unknown fields are an error, so a typo in a field name never silently drops data.
typerequired. thesis, duel, poll, collection or posttitlerequired. Thesis up to 140 characters, everything else up to 200tagrequired. One of GET /tagsoptionsanswers: duel exactly 2, poll 2-10, collection 2-30, each up to 200 characters. Not used by thesis and postmax_choicespoll only. How many answers one reader may pick; 1 or empty means single choiceshow_resultstrue shows the split before the reader votes; by default only voters see the numbersnsfwtrue marks the post 18+ (see below)bodypost only. Text in rupor markup, up to 40 000 charactersimagespicture tokens from POST /media. post: up to 10, the gallery. thesis, duel, poll: 1, the cover. collection: not usedoption_imagescollection only. One token per option in the same order; an empty string means no picture for that itemsource_urllink to the article on your verified domain, shown under the post as SourceThesis
One statement, readers answer yes or no. The answers are fixed, options are not sent. The full request:
curl -X POST https://rupor.me/api/v1/posts \
-H "Authorization: Bearer $RUPOR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"type":"thesis","tag":"movies",
"title":"Pineapple belongs on pizza"}'
Duel
Two named sides. options takes exactly two. Only the JSON body is shown below; the headers are the same as for the thesis.
{"type":"duel","tag":"movies",
"title":"Paper books or an e-reader?",
"options":["Paper","E-reader"]}
Poll
From 2 to 10 answers. max_choices lets one reader pick several, here up to two. show_results shows the split before voting.
{"type":"poll","tag":"movies",
"title":"Movie night snack: what is non-negotiable?",
"options":["Popcorn","Nachos","Chocolate","Nothing, I just watch"],
"max_choices":2,
"show_results":true}
Collection (ranking)
A ranking: from 2 to 30 items, readers vote for every item they like and the list can be sorted by their votes. Each item can have its own picture.
Upload pictures with POST /media first, then put the tokens into option_images in the same order as options. An empty string leaves an item without a picture.
{"type":"collection","tag":"movies",
"title":"Best pizza toppings, ranked",
"options":["Pepperoni","Mushrooms","Pineapple"],
"option_images":["<token 1>","<token 2>",""]}
Text post
A post without a vote: a title, a body and up to 10 pictures. The first picture is the cover in the feed.
The body uses rupor markup: **bold**, *italic*, ~~strikethrough~~, [link text](https://address), a line starting with > is a quote, a line starting with - is a list item, >!spoiler!< hides text until a tap. [img:N] puts the N-th picture of images at that spot, otherwise pictures go after the text as a gallery.
HTML is never rendered: tags show up as plain text. Links only go to http and https.
{"type":"post","tag":"movies",
"title":"We tested 5 e-readers for a month",
"body":"**Short version:** paper still wins.\n\n[img:1]\n\n> The battery lasted 6 weeks\n\n- light\n- cheap\n\nFull review [on our site](https://yoursite.com/review)",
"images":["<token 1>","<token 2>"]}
Pictures
POST /media takes one JPEG or PNG up to 5 MB in the multipart field file. We check the format by content, re-encode it to WebP and strip metadata.
The answer has a token. Use it in images or option_images when creating a post. A token works only for your own page. A picture that no post uses is deleted after a while.
Limit: 50 pictures a day, 500 with a verified domain.
curl -X POST https://rupor.me/api/v1/media \ -H "Authorization: Bearer $RUPOR_KEY" \ -F "file=@photo.jpg"
HTTP/1.1 201 Created
{"token":"a1b2c3...","width":1600,"height":1067,"url":"https://rupor.me/uploads/a1b2c3.../w1600.webp"}
18+ content
Dating, sex and drinking topics are allowed if the post is marked "nsfw": true. Such a post gets an 18+ badge; its picture, text and answers stay blurred until the reader confirms being 18 or older. The title stays visible, so keep it frank but not graphic.
Never allowed, with or without the flag: nudity and pornography, anything involving minors, real people named without consent. An unmarked 18+ post can be marked or removed by moderators.
{"type":"thesis","tag":"movies","title":"Sleeping together on the first date is fine","nsfw":true}
Link to your article
source_url puts "Source: yoursite.com" under the post, linking to the article on your site. It works only for pages with a verified domain, and only to that domain or its subdomains. Any other address is refused.
{"type":"duel","tag":"movies","title":"...","options":["...","..."],
"source_url":"https://yoursite.com/2026/10/paper-vs-ereader"}
Reading posts and results
GET /posts returns your page's posts, newest first: up to 50 per call (limit), and next_before for the next batch.
GET /posts/{id} adds vote results. status is published, in_review (waiting for moderation) or hidden (hidden by moderators).
curl "https://rupor.me/api/v1/posts?limit=20" -H "Authorization: Bearer $RUPOR_KEY" curl "https://rupor.me/api/v1/posts?limit=20&before=123" -H "Authorization: Bearer $RUPOR_KEY" curl https://rupor.me/api/v1/posts/123 -H "Authorization: Bearer $RUPOR_KEY"
{"id":123,"type":"duel","status":"published","url":"https://rupor.me/s/paper-books-or-an-e-reader-123",
"votes":412,"comments":37,"fires":58,
"options":[{"text":"Paper","votes":251},{"text":"E-reader","votes":161}]}
Editing and deleting
PATCH can change title, body, tag, nsfw and source_url for 15 minutes after posting and only before the first vote or comment. Send only the fields you change. The answers of a vote are never edited: people may already have voted. An empty source_url removes the link.
DELETE removes your post at any time.
curl -X PATCH https://rupor.me/api/v1/posts/123 \
-H "Authorization: Bearer $RUPOR_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Paper books or e-reader: which do you use?","nsfw":false}'
curl -X DELETE https://rupor.me/api/v1/posts/123 -H "Authorization: Bearer $RUPOR_KEY"
Retries without duplicates
POST /posts needs an Idempotency-Key header: a new random value for every new post (a UUID is fine). If your request timed out and you send it again with the same value within 24 hours, you get the post that was already created, with the header Idempotent-Replayed: true, instead of a second copy.
The same title from the same page twice within 24 hours is refused with 409.
Limits and review
Over a limit the answer is 429 with Retry-After in seconds. Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining.
The first 10 posts of a page without a verified domain go to review before they appear in the feed (status in_review). After that, or with a verified domain, posts go live at once.
requests60 a minute per key, 300 with a verified domainnew posts5 an hour and 20 a day per page; 30 and 200 with a verified domainpictures50 a day per page, 500 with a verified domainwrong keys20 a minute from one address, then a pausetitle lengththesis up to 140 characters, every other type up to 200post textup to 40 000 characters, about 6 000 words: long reads are welcomeanswer lengtheach option up to 200 charactersanswers per postduel 2, poll 2-10, collection 2-30pictures per postpost up to 10, thesis, duel and poll 1, collection 1 per itempicture fileJPEG or PNG up to 5 MBrequest bodyup to 64 KB of JSONContent rules
Posts from the API follow the same site rules as posts from people. Politics and news are off-topic here. Spam, ads disguised as posts and mass reposting lead to the page being frozen, which disables all its keys.
Invisible characters and text direction switches are removed from all text. Votes and comments come only from people, never through the API.
Errors
Every error is JSON with one error field that says what to fix.
400malformed request, unknown field, missing Idempotency-Key, key in the URL401missing, unknown or revoked key403the key has no permission for this (a read only key tried to post)404no such post on your page409duplicate title within 24 hours, or too late to edit413body over 64 KB or picture over 5 MB415wrong Content-Type: JSON for posts, multipart for pictures422a field breaks the rules: the text says which one429a limit was reached, wait Retry-After secondsHTTP/1.1 422 Unprocessable Entity
{"error":"options: duel needs exactly 2"}
OpenAPI
A machine description of all methods and fields, for generating a client in your language:
https://rupor.me/api/v1/openapi.json