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 review
GET /typespost types and the rules for their fields, machine-readable
GET /tagstags you can use
POST /mediaupload a picture, get a token for images or option_images
POST /postscreate a post (Idempotency-Key header required)
GET /postsyour page's posts, newest first
GET /posts/{id}one of your posts with vote results
PATCH /posts/{id}edit within 15 minutes, before the first vote or comment
DELETE /posts/{id}delete one of your posts
GET /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 post
titlerequired. Thesis up to 140 characters, everything else up to 200
tagrequired. One of GET /tags
optionsanswers: duel exactly 2, poll 2-10, collection 2-30, each up to 200 characters. Not used by thesis and post
max_choicespoll only. How many answers one reader may pick; 1 or empty means single choice
show_resultstrue shows the split before the reader votes; by default only voters see the numbers
nsfwtrue marks the post 18+ (see below)
bodypost only. Text in rupor markup, up to 40 000 characters
imagespicture tokens from POST /media. post: up to 10, the gallery. thesis, duel, poll: 1, the cover. collection: not used
option_imagescollection only. One token per option in the same order; an empty string means no picture for that item
source_urllink to the article on your verified domain, shown under the post as Source

Thesis

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 domain
new posts5 an hour and 20 a day per page; 30 and 200 with a verified domain
pictures50 a day per page, 500 with a verified domain
wrong keys20 a minute from one address, then a pause
title lengththesis up to 140 characters, every other type up to 200
post textup to 40 000 characters, about 6 000 words: long reads are welcome
answer lengtheach option up to 200 characters
answers per postduel 2, poll 2-10, collection 2-30
pictures per postpost up to 10, thesis, duel and poll 1, collection 1 per item
picture fileJPEG or PNG up to 5 MB
request bodyup to 64 KB of JSON

Content 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 URL
401missing, unknown or revoked key
403the key has no permission for this (a read only key tried to post)
404no such post on your page
409duplicate title within 24 hours, or too late to edit
413body over 64 KB or picture over 5 MB
415wrong Content-Type: JSON for posts, multipart for pictures
422a field breaks the rules: the text says which one
429a limit was reached, wait Retry-After seconds
HTTP/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

We only use essential cookies: to keep you signed in, count votes fairly and protect forms. No ads, no tracking. Details