platforms/devplace/devrant_compatible_api.md

DevPlace devRant-Compatible REST API

The devRant compatibility layer: base URL and response shape, integer id mapping, the token-triple authentication model, and the rants, comments, users and notifications endpoints, with ready-made client scripts.

Overview

DevPlace exposes a second REST protocol under https://devplace.net/api that reproduces the public devRant API shape on DevPlace data, so legacy devRant clients can run against this server unchanged. Rants are DevPlace posts, comments and votes are the native engagement layer, and every devRant action is funnelled through the same audited helpers as the website (so XP, notifications, and soft-delete all apply).

This reference is split across focused pages:

Base URL and shape

Every endpoint lives under https://devplace.net/api and returns JSON with a success boolean. On success the payload sits beside it; on a logical failure the response is { "success": false, "error": "..." }. HTTP status is 200 for logical failures, except a bad login which returns 400 (matching devRant). Requests accept parameters as query string (GET/DELETE) or as a form body or JSON body (POST).

Integer IDs

devRant identifies everything by integer. DevPlace maps those directly onto the auto-increment id that every table already carries, so rantid is a post's id, commentid is a comment's id, and user_id is a user's id. There is no separate id space to track.

Authentication model

Write operations need the devRant token triple. POST /api/users/auth-token with a username (or email) and password returns an authtoken object; you then send userid, tokenid, and tokenkey with every request. Read endpoints (feed, single rant, search, profiles) work without authentication. See Authentication & accounts.

Field mapping at a glance

devRant conceptDevPlace mapping
rantpost (topic forced to rant; text is title + body when a title exists)
tagsstored verbatim on the post and returned as-is (falls back to [topic])
commentcomment with target_type = post
vote (1/-1/0)upvote / downvote / clear on the native vote layer
favorite / unfavoritebookmark add / remove
user_avatara real PNG rendered from the username (see Users & avatars)
profile.skillsderived from the user bio (DevPlace has no separate skills field)

Availability

The protocol is toggled by the devrantapienabled site setting (default on); when off, every /api path returns 404. Legacy clients hard-coded to devrant.com reach this server only through host routing (DNS / reverse-proxy), which is an infrastructure concern.

Authentication and accounts

Write operations authenticate with the devRant token triple. Log in once below and every authenticated widget across these pages becomes runnable (the token is kept in your browser only). Read operations (rants, profiles) need no authentication.

POST /api/users/auth-token returns an authtoken whose id is the tokenid, key is the tokenkey, and userid is the integer user id; those three are sent automatically by these widgets (query params for GET/DELETE, form body for POST). A bad login returns HTTP 400. See the overview for the response envelope.

A DevRant auth token (the key field) also works on the main DevPlace API: use it as a Bearer token or X-API-KEY header on any DevPlace endpoint. See Authentication for details.

DevPlace also has its own native token endpoint at POST /auth/token - see the Authentication page. </div>

<div class="devrant-login-bar" data-devrant-login></div>

<section class="endpoint-card" id="devrant-login"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/users/auth-token</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Log in</h3> <p class="endpoint-summary">Authenticate with username (or email) and password. Running this here logs you in for every widget on these pages.</p> <div data-devrant-tester data-config='{"auth": "public", "destructive": false, "encoding": "form", "id": "devrant-login", "method": "POST", "minrole": "Public", "params": [{"description": "Username or email.", "example": "YOURUSERNAME", "location": "body", "name": "username", "required": true, "type": "string"}, {"description": "Account password.", "example": "", "location": "body", "name": "password", "required": true, "type": "password"}], "path": "/api/users/auth-token", "sampleresponse": {"authtoken": {"expiretime": 1782024794, "id": 18966518, "key": "z6uXRZrQ...", "userid": 42}, "success": true}, "summary": "Authenticate with username (or email) and password. Running this here logs you in for every widget on these pages.", "title": "Log in"}'></div> </section>

<section class="endpoint-card" id="devrant-register"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/users</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Register</h3> <p class="endpoint-summary">Create a new account. Returns an auth token (you are logged in immediately).</p> <div data-devrant-tester data-config='{"auth": "public", "destructive": false, "encoding": "form", "id": "devrant-register", "method": "POST", "minrole": "Public", "params": [{"description": "3-32 letters, numbers, hyphens, underscores.", "example": "newdev", "location": "body", "name": "username", "required": true, "type": "string"}, {"description": "Valid email address.", "example": "newdev@example.com", "location": "body", "name": "email", "required": true, "type": "string"}, {"description": "At least 6 characters.", "example": "", "location": "body", "name": "password", "required": true, "type": "password"}], "path": "/api/users", "sampleresponse": {"authtoken": {"expiretime": 1782024794, "id": 18966518, "key": "z6uXRZrQ...", "user_id": 42}, "success": true}, "summary": "Create a new account. Returns an auth token (you are logged in immediately).", "title": "Register"}'></div> </section>

<section class="endpoint-card" id="devrant-delete-account"> <header class="endpoint-head"> <span class="method-badge method-delete">DELETE</span> <code class="endpoint-path">/api/users/me</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Deactivate account</h3> <p class="endpoint-summary">Deactivate the logged-in account and revoke its tokens.</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": true, "encoding": "none", "id": "devrant-delete-account", "method": "DELETE", "minrole": "Member", "params": [], "path": "/api/users/me", "sampleresponse": {"success": true}, "summary": "Deactivate the logged-in account and revoke its tokens.", "title": "Deactivate account"}'></div> </section>

Rants

A rant is a DevPlace post. Reads are public; writes need the token triple - log in with the bar below (or on the Authentication page) and run any widget live. The token triple is injected automatically. text is the post's title and body combined; devRant tags round-trip verbatim. Posting a comment lives here too; editing and voting on a comment is on the Comments page. </div>

<div class="devrant-login-bar" data-devrant-login></div>

<section class="endpoint-card" id="devrant-feed"> <header class="endpoint-head"> <span class="method-badge method-get">GET</span> <code class="endpoint-path">/api/devrant/rants</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Rant feed</h3> <p class="endpoint-summary">List rants. Sort by recent, top, or algo.</p> <div data-devrant-tester data-config='{"auth": "public", "destructive": false, "encoding": "none", "id": "devrant-feed", "method": "GET", "minrole": "Public", "params": [{"description": "Sort order.", "example": "recent", "location": "query", "name": "sort", "options": ["recent", "top", "algo"], "required": false, "type": "enum"}, {"description": "Page size, 1-50.", "example": "20", "location": "query", "name": "limit", "required": false, "type": "int"}, {"description": "Offset for pagination.", "example": "0", "location": "query", "name": "skip", "required": false, "type": "int"}], "path": "/api/devrant/rants", "sampleresponse": {"numnotifs": 0, "rants": [{"attachedimage": "", "createdtime": 1781419994, "editable": false, "edited": false, "id": 1, "link": "rants/1/my-first-rant-about-python", "numcomments": 2, "rc": 0, "rt": 1, "score": 3, "tags": ["python", "devrant"], "text": "My first rant about Python", "useravatar": {"b": "5c47fe", "i": "u/alice.png"}, "useravatarlg": {"b": "5c47fe", "i": "u/alice.png"}, "userid": 42, "userscore": 17, "userusername": "alice", "vote_state": 0}], "success": true}, "summary": "List rants. Sort by recent, top, or algo.", "title": "Rant feed"}'></div> </section>

<section class="endpoint-card" id="devrant-get-rant"> <header class="endpoint-head"> <span class="method-badge method-get">GET</span> <code class="endpoint-path">/api/devrant/rants/{rantid}</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Single rant with comments</h3> <p class="endpoint-summary">Fetch one rant and its comments.</p> <div data-devrant-tester data-config='{"auth": "public", "destructive": false, "encoding": "none", "id": "devrant-get-rant", "method": "GET", "minrole": "Public", "params": [{"description": "Rant id.", "example": "1", "location": "path", "name": "rantid", "required": true, "type": "int"}], "path": "/api/devrant/rants/{rantid}", "sampleresponse": {"comments": [{"body": "Nice rant!", "createdtime": 1781420050, "id": 9, "rantid": 1, "score": 0, "useravatar": {"b": "1188ff", "i": "u/bob.png"}, "userid": 7, "userscore": 4, "userusername": "bob", "votestate": 0}], "rant": {"attachedimage": "", "createdtime": 1781419994, "editable": false, "edited": false, "id": 1, "link": "rants/1/my-first-rant-about-python", "numcomments": 2, "rc": 0, "rt": 1, "score": 3, "tags": ["python", "devrant"], "text": "My first rant about Python", "useravatar": {"b": "5c47fe", "i": "u/alice.png"}, "useravatarlg": {"b": "5c47fe", "i": "u/alice.png"}, "userid": 42, "userscore": 17, "userusername": "alice", "votestate": 0}, "subscribed": 0, "success": true}, "summary": "Fetch one rant and its comments.", "title": "Single rant with comments"}'></div> </section>

<section class="endpoint-card" id="devrant-create-rant"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/devrant/rants</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Post a rant</h3> <p class="endpoint-summary">Create a new rant. Tags are comma-separated and stored verbatim.</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-create-rant", "method": "POST", "minrole": "Member", "params": [{"description": "Rant text, 1-125000 chars.", "example": "Posted from the docs widget", "location": "body", "name": "rant", "required": true, "type": "textarea"}, {"description": "Comma-separated tags.", "example": "python,devrant", "location": "body", "name": "tags", "required": false, "type": "string"}], "path": "/api/devrant/rants", "sampleresponse": {"rant_id": 12, "success": true}, "summary": "Create a new rant. Tags are comma-separated and stored verbatim.", "title": "Post a rant"}'></div> </section>

<section class="endpoint-card" id="devrant-edit-rant"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/devrant/rants/{rantid}</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Edit a rant</h3> <p class="endpoint-summary">Replace a rant&#39;s text and tags (owner only).</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-edit-rant", "method": "POST", "minrole": "Member", "params": [{"description": "Rant id.", "example": "1", "location": "path", "name": "rantid", "required": true, "type": "int"}, {"description": "New rant text.", "example": "Edited text", "location": "body", "name": "rant", "required": true, "type": "textarea"}, {"description": "Comma-separated tags.", "example": "python", "location": "body", "name": "tags", "required": false, "type": "string"}], "path": "/api/devrant/rants/{rantid}", "sample_response": {"success": true}, "summary": "Replace a rant\u0027s text and tags (owner only).", "title": "Edit a rant"}'></div> </section>

<section class="endpoint-card" id="devrant-delete-rant"> <header class="endpoint-head"> <span class="method-badge method-delete">DELETE</span> <code class="endpoint-path">/api/devrant/rants/{rantid}</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Delete a rant</h3> <p class="endpoint-summary">Soft-delete a rant (owner or admin).</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": true, "encoding": "none", "id": "devrant-delete-rant", "method": "DELETE", "minrole": "Member", "params": [{"description": "Rant id.", "example": "1", "location": "path", "name": "rantid", "required": true, "type": "int"}], "path": "/api/devrant/rants/{rantid}", "sample_response": {"success": true}, "summary": "Soft-delete a rant (owner or admin).", "title": "Delete a rant"}'></div> </section>

<section class="endpoint-card" id="devrant-vote-rant"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/devrant/rants/{rantid}/vote</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Vote on a rant</h3> <p class="endpoint-summary">Upvote (1), downvote (-1), or clear (0). Returns the updated rant.</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-vote-rant", "method": "POST", "minrole": "Member", "params": [{"description": "Rant id.", "example": "1", "location": "path", "name": "rantid", "required": true, "type": "int"}, {"description": "Vote value.", "example": "1", "location": "body", "name": "vote", "options": ["1", "-1", "0"], "required": true, "type": "enum"}], "path": "/api/devrant/rants/{rantid}/vote", "sampleresponse": {"rant": {"attachedimage": "", "createdtime": 1781419994, "editable": false, "edited": false, "id": 1, "link": "rants/1/my-first-rant-about-python", "numcomments": 2, "rc": 0, "rt": 1, "score": 3, "tags": ["python", "devrant"], "text": "My first rant about Python", "useravatar": {"b": "5c47fe", "i": "u/alice.png"}, "useravatarlg": {"b": "5c47fe", "i": "u/alice.png"}, "userid": 42, "userscore": 17, "userusername": "alice", "vote_state": 0}, "success": true}, "summary": "Upvote (1), downvote (-1), or clear (0). Returns the updated rant.", "title": "Vote on a rant"}'></div> </section>

<section class="endpoint-card" id="devrant-favorite"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/devrant/rants/{rantid}/favorite</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Favorite a rant</h3> <p class="endpoint-summary">Bookmark a rant.</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-favorite", "method": "POST", "minrole": "Member", "params": [{"description": "Rant id.", "example": "1", "location": "path", "name": "rantid", "required": true, "type": "int"}], "path": "/api/devrant/rants/{rantid}/favorite", "sample_response": {"success": true}, "summary": "Bookmark a rant.", "title": "Favorite a rant"}'></div> </section>

<section class="endpoint-card" id="devrant-unfavorite"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/devrant/rants/{rantid}/unfavorite</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Unfavorite a rant</h3> <p class="endpoint-summary">Remove a rant bookmark.</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-unfavorite", "method": "POST", "minrole": "Member", "params": [{"description": "Rant id.", "example": "1", "location": "path", "name": "rantid", "required": true, "type": "int"}], "path": "/api/devrant/rants/{rantid}/unfavorite", "sample_response": {"success": true}, "summary": "Remove a rant bookmark.", "title": "Unfavorite a rant"}'></div> </section>

<section class="endpoint-card" id="devrant-comment-rant"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/devrant/rants/{rantid}/comments</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Comment on a rant</h3> <p class="endpoint-summary">Post a comment on a rant.</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-comment-rant", "method": "POST", "minrole": "Member", "params": [{"description": "Rant id.", "example": "1", "location": "path", "name": "rantid", "required": true, "type": "int"}, {"description": "Comment text, 1-125000 chars.", "example": "Great rant!", "location": "body", "name": "comment", "required": true, "type": "textarea"}], "path": "/api/devrant/rants/{rantid}/comments", "sample_response": {"success": true}, "summary": "Post a comment on a rant.", "title": "Comment on a rant"}'></div> </section>

<section class="endpoint-card" id="devrant-search"> <header class="endpoint-head"> <span class="method-badge method-get">GET</span> <code class="endpoint-path">/api/devrant/search</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Search rants</h3> <p class="endpoint-summary">Search rants by text in the title and body.</p> <div data-devrant-tester data-config='{"auth": "public", "destructive": false, "encoding": "none", "id": "devrant-search", "method": "GET", "minrole": "Public", "params": [{"description": "Search text.", "example": "python", "location": "query", "name": "term", "required": true, "type": "string"}], "path": "/api/devrant/search", "sampleresponse": {"results": [{"attachedimage": "", "createdtime": 1781419994, "editable": false, "edited": false, "id": 1, "link": "rants/1/my-first-rant-about-python", "numcomments": 2, "rc": 0, "rt": 1, "score": 3, "tags": ["python", "devrant"], "text": "My first rant about Python", "useravatar": {"b": "5c47fe", "i": "u/alice.png"}, "useravatarlg": {"b": "5c47fe", "i": "u/alice.png"}, "userid": 42, "userscore": 17, "userusername": "alice", "votestate": 0}], "success": true}, "summary": "Search rants by text in the title and body.", "title": "Search rants"}'></div> </section>

Comments

A comment is a DevPlace comment on a post, identified by its integer id. Posting a comment on a rant is on the Rants page; the widgets below read, edit, delete, and vote on an existing comment. Log in below to enable the authenticated widgets. </div>

<div class="devrant-login-bar" data-devrant-login></div>

<section class="endpoint-card" id="devrant-get-comment"> <header class="endpoint-head"> <span class="method-badge method-get">GET</span> <code class="endpoint-path">/api/comments/{commentid}</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Get a comment</h3> <p class="endpoint-summary">Fetch a single comment by id.</p> <div data-devrant-tester data-config='{"auth": "public", "destructive": false, "encoding": "none", "id": "devrant-get-comment", "method": "GET", "minrole": "Public", "params": [{"description": "Comment id.", "example": "1", "location": "path", "name": "commentid", "required": true, "type": "int"}], "path": "/api/comments/{commentid}", "sampleresponse": {"comment": {"body": "Nice rant!", "createdtime": 1781420050, "id": 9, "rantid": 1, "score": 0, "useravatar": {"b": "1188ff", "i": "u/bob.png"}, "userid": 7, "userscore": 4, "userusername": "bob", "votestate": 0}, "success": true}, "summary": "Fetch a single comment by id.", "title": "Get a comment"}'></div> </section>

<section class="endpoint-card" id="devrant-edit-comment"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/comments/{commentid}</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Edit a comment</h3> <p class="endpoint-summary">Replace a comment&#39;s text (owner only).</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-edit-comment", "method": "POST", "minrole": "Member", "params": [{"description": "Comment id.", "example": "1", "location": "path", "name": "commentid", "required": true, "type": "int"}, {"description": "New comment text.", "example": "Edited comment", "location": "body", "name": "comment", "required": true, "type": "textarea"}], "path": "/api/comments/{commentid}", "sample_response": {"success": true}, "summary": "Replace a comment\u0027s text (owner only).", "title": "Edit a comment"}'></div> </section>

<section class="endpoint-card" id="devrant-delete-comment"> <header class="endpoint-head"> <span class="method-badge method-delete">DELETE</span> <code class="endpoint-path">/api/comments/{commentid}</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Delete a comment</h3> <p class="endpoint-summary">Soft-delete a comment (owner or admin).</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": true, "encoding": "none", "id": "devrant-delete-comment", "method": "DELETE", "minrole": "Member", "params": [{"description": "Comment id.", "example": "1", "location": "path", "name": "commentid", "required": true, "type": "int"}], "path": "/api/comments/{commentid}", "sample_response": {"success": true}, "summary": "Soft-delete a comment (owner or admin).", "title": "Delete a comment"}'></div> </section>

<section class="endpoint-card" id="devrant-vote-comment"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/comments/{commentid}/vote</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Vote on a comment</h3> <p class="endpoint-summary">Upvote (1), downvote (-1), or clear (0).</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-vote-comment", "method": "POST", "minrole": "Member", "params": [{"description": "Comment id.", "example": "1", "location": "path", "name": "commentid", "required": true, "type": "int"}, {"description": "Vote value.", "example": "1", "location": "body", "name": "vote", "options": ["1", "-1", "0"], "required": true, "type": "enum"}], "path": "/api/comments/{commentid}/vote", "sample_response": {"success": true}, "summary": "Upvote (1), downvote (-1), or clear (0).", "title": "Vote on a comment"}'></div> </section>

Users and avatars

Profiles are public; editing your own needs the token triple (log in below). score is the user's net stars, about is the bio, github is the git link, and skills is derived from the bio. The nested content.content holds the user's serialized rants and comments. </div>

<div class="devrant-login-bar" data-devrant-login></div>

<section class="endpoint-card" id="devrant-get-user-id"> <header class="endpoint-head"> <span class="method-badge method-get">GET</span> <code class="endpoint-path">/api/get-user-id</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Resolve username to id</h3> <p class="endpoint-summary">Look up a user&#39;s integer id from their username.</p> <div data-devrant-tester data-config='{"auth": "public", "destructive": false, "encoding": "none", "id": "devrant-get-user-id", "method": "GET", "minrole": "Public", "params": [{"description": "Username to look up.", "example": "alice", "location": "query", "name": "username", "required": true, "type": "string"}], "path": "/api/get-user-id", "sampleresponse": {"success": true, "user_id": 42}, "summary": "Look up a user\u0027s integer id from their username.", "title": "Resolve username to id"}'></div> </section>

<section class="endpoint-card" id="devrant-profile"> <header class="endpoint-head"> <span class="method-badge method-get">GET</span> <code class="endpoint-path">/api/users/{userid}</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Get a profile</h3> <p class="endpoint-summary">Fetch a user&#39;s devRant profile with their rants and comments.</p> <div data-devrant-tester data-config='{"auth": "public", "destructive": false, "encoding": "none", "id": "devrant-profile", "method": "GET", "minrole": "Public", "params": [{"description": "User id.", "example": "1", "location": "path", "name": "userid", "required": true, "type": "int"}], "path": "/api/users/{userid}", "sampleresponse": {"profile": {"about": "I build things with Python", "avatar": {"b": "5c47fe", "i": "u/alice.png"}, "content": {"content": {"comments": [{"body": "Nice rant!", "createdtime": 1781420050, "id": 9, "rantid": 1, "score": 0, "useravatar": {"b": "1188ff", "i": "u/bob.png"}, "userid": 7, "userscore": 4, "userusername": "bob", "votestate": 0}], "favorites": [], "rants": [{"attachedimage": "", "createdtime": 1781419994, "editable": false, "edited": false, "id": 1, "link": "rants/1/my-first-rant-about-python", "numcomments": 2, "rc": 0, "rt": 1, "score": 3, "tags": ["python", "devrant"], "text": "My first rant about Python", "useravatar": {"b": "5c47fe", "i": "u/alice.png"}, "useravatarlg": {"b": "5c47fe", "i": "u/alice.png"}, "userid": 42, "userscore": 17, "userusername": "alice", "votestate": 0}], "upvoted": [], "viewed": []}, "counts": {"collabs": 0, "comments": 1, "favorites": 0, "rants": 1, "upvoted": 0}}, "created_time": 1781419994, "github": "alice", "location": "Amsterdam", "score": 17, "skills": "I build things with Python", "username": "alice", "website": "https://alice.dev"}, "success": true}, "summary": "Fetch a user\u0027s devRant profile with their rants and comments.", "title": "Get a profile"}'></div> </section>

<section class="endpoint-card" id="devrant-edit-profile"> <header class="endpoint-head"> <span class="method-badge method-post">POST</span> <code class="endpoint-path">/api/users/me/edit-profile</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Edit your profile</h3> <p class="endpoint-summary">Update bio, location, git link, and website. profileskills is accepted but ignored (skills are derived from the bio).</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "form", "id": "devrant-edit-profile", "method": "POST", "minrole": "Member", "params": [{"description": "Bio.", "example": "I build with Python and Rust", "location": "body", "name": "profileabout", "required": false, "type": "textarea"}, {"description": "Location.", "example": "Amsterdam", "location": "body", "name": "profilelocation", "required": false, "type": "string"}, {"description": "Git link.", "example": "alice", "location": "body", "name": "profilegithub", "required": false, "type": "string"}, {"description": "Website.", "example": "https://alice.dev", "location": "body", "name": "profilewebsite", "required": false, "type": "string"}], "path": "/api/users/me/edit-profile", "sampleresponse": {"success": true}, "summary": "Update bio, location, git link, and website. profileskills is accepted but ignored (skills are derived from the bio).", "title": "Edit your profile"}'></div> </section>

<section class="endpoint-card" id="devrant-avatar"> <header class="endpoint-head"> <span class="method-badge method-get">GET</span> <code class="endpoint-path">/api/avatars/u/{username}.png</code> <span class="auth-badge auth-public" title="Minimal role required to call this endpoint">Minimal role: Public</span> </header> <h3 class="endpoint-title">Avatar image</h3> <p class="endpoint-summary">Every <code>user_avatar.i</code> points at this path. It renders a real PNG from the username seed; pass <code>?size=</code> (16-512, default 128).</p> <div class="api-tester"> <img src="/api/avatars/u/devplace.png?size=96" alt="Example devRant avatar" width="96" height="96" class="docs-demo-avatar"> </div> </section>

Notifications

The notification feed maps DevPlace notifications onto the devRant shape. Both endpoints need the token triple - log in below. DevPlace types map onto devRant types: comment/reply to commentdiscuss, mention to commentmention, vote to rantvote, follow to rantsub. </div>

<div class="devrant-login-bar" data-devrant-login></div>

<section class="endpoint-card" id="devrant-notif-feed"> <header class="endpoint-head"> <span class="method-badge method-get">GET</span> <code class="endpoint-path">/api/users/me/notif-feed</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Notification feed</h3> <p class="endpoint-summary">Fetch the logged-in user&#39;s notification feed with unread counts.</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": false, "encoding": "none", "id": "devrant-notif-feed", "method": "GET", "minrole": "Member", "params": [], "path": "/api/users/me/notif-feed", "sampleresponse": {"data": {"checktime": 1781420060, "items": [{"commentid": 0, "createdtime": 1781420050, "rantid": 0, "read": 0, "type": "commentdiscuss", "uid": 7, "username": "bob"}], "numunread": 1, "unread": {"all": 1, "comments": 1, "mentions": 0, "subs": 0, "total": 1, "upvotes": 0}, "username_map": {"7": "bob"}}, "success": true}, "summary": "Fetch the logged-in user\u0027s notification feed with unread counts.", "title": "Notification feed"}'></div> </section>

<section class="endpoint-card" id="devrant-clear-notif-feed"> <header class="endpoint-head"> <span class="method-badge method-delete">DELETE</span> <code class="endpoint-path">/api/users/me/notif-feed</code> <span class="auth-badge auth-user" title="Minimal role required to call this endpoint">Minimal role: Member</span> </header> <h3 class="endpoint-title">Clear notifications</h3> <p class="endpoint-summary">Mark every notification read.</p> <div data-devrant-tester data-config='{"auth": "user", "destructive": true, "encoding": "none", "id": "devrant-clear-notif-feed", "method": "DELETE", "minrole": "Member", "params": [], "path": "/api/users/me/notif-feed", "sampleresponse": {"success": true}, "summary": "Mark every notification read.", "title": "Clear notifications"}'></div> </section>

Client scripts

Ready-to-run clients for the devRant-compatible protocol. The Python client uses only the standard library; the JavaScript client uses Node 18+ (global fetch). The full versions, plus example scripts and an end-to-end conformance test, ship in the repository under examples/devrant/.

Python client (drop-in)

import json, urllib.parse, urllib.request


class DevRant:
    def __init__(self, base_url, username=None, password=None):
        self.base_url = base_url.rstrip("/")
        self.username, self.password = username, password
        self.auth = {}

    def _request(self, method, path, params=None, body=None):
        merged = dict(params or {}); merged.update(self.auth)
        url = f"{self.base_url}/api/{path.lstrip('/')}"
        data, headers = None, {"Accept": "application/json"}
        if method in ("GET", "DELETE"):
            if merged:
                url += "?" + urllib.parse.urlencode(merged)
        else:
            payload = dict(merged); payload.update(body or {})
            data = urllib.parse.urlencode(payload).encode()
            headers["Content-Type"] = "application/x-www-form-urlencoded"
        req = urllib.request.Request(url, data=data, headers=headers, method=method)
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read().decode())

    def login(self):
        out = self._request("POST", "users/auth-token",
                            body={"username": self.username, "password": self.password})
        if not out.get("success"):
            raise RuntimeError(out.get("error", "login failed"))
        t = out["auth_token"]
        self.auth = {"user_id": t["user_id"], "token_id": t["id"], "token_key": t["key"]}
        return self.auth

    def rants(self, sort="recent", limit=20, skip=0):
        return self._request("GET", "devrant/rants",
                            {"sort": sort, "limit": limit, "skip": skip}).get("rants", [])

    def post_rant(self, text, tags=""):
        return self._request("POST", "devrant/rants", body={"rant": text, "tags": tags})

    def vote_rant(self, rant_id, vote):
        return self._request("POST", f"devrant/rants/{rant_id}/vote", body={"vote": vote})

    def comment(self, rant_id, text):
        return self._request("POST", f"devrant/rants/{rant_id}/comments", body={"comment": text})


api = DevRant("https://devplace.net", "USERNAME", "PASSWORD")
api.login()
print(api.post_rant("Hello from Python", "python,devrant"))

JavaScript client (drop-in)

export class DevRant {
  constructor(baseUrl, username, password) {
    this.baseUrl = baseUrl.replace(/\/$/, "");
    this.username = username; this.password = password; this.auth = {};
  }
  async _request(method, path, params = {}, body = null) {
    const merged = { ...params, ...this.auth };
    const headers = { Accept: "application/json" };
    let url = new URL(`${this.baseUrl}/api/${path.replace(/^\//, "")}`);
    const init = { method, headers };
    if (method === "GET" || method === "DELETE") {
      for (const [k, v] of Object.entries(merged)) url.searchParams.set(k, v);
    } else {
      headers["Content-Type"] = "application/x-www-form-urlencoded";
      init.body = new URLSearchParams({ ...merged, ...(body || {}) }).toString();
    }
    return (await fetch(url, init)).json();
  }
  async login() {
    const out = await this._request("POST", "users/auth-token", {},
      { username: this.username, password: this.password });
    if (!out.success) throw new Error(out.error || "login failed");
    const t = out.auth_token;
    this.auth = { user_id: t.user_id, token_id: t.id, token_key: t.key };
    return this.auth;
  }
  async rants(sort = "recent", limit = 20, skip = 0) {
    return (await this._request("GET", "devrant/rants", { sort, limit, skip })).rants || [];
  }
  postRant(text, tags = "") {
    return this._request("POST", "devrant/rants", {}, { rant: text, tags });
  }
  voteRant(rantId, vote) {
    return this._request("POST", `devrant/rants/${rantId}/vote`, {}, { vote });
  }
  comment(rantId, text) {
    return this._request("POST", `devrant/rants/${rantId}/comments`, {}, { comment: text });
  }
}

const api = new DevRant("https://devplace.net", "USERNAME", "PASSWORD");
await api.login();
console.log(await api.postRant("Hello from Node", "javascript,devrant"));

Example scripts in the repository

examples/devrant/ contains the complete clients and runnable scripts:

FileLanguageWhat it does
client.py / client.mjsPython / JSFull reusable client (every endpoint).
postrant.py / postrant.mjsPython / JSPost one rant from the command line.
feedwatch.py / feedwatch.mjsPython / JSLive feed ticker, optional keyword auto-upvote.
smoketest.py / smoketest.mjsPython / JSEnd-to-end conformance test, prints PASS/FAIL.

They read DEVRANTBASE, DEVRANTUSERNAME, and DEVRANT_PASSWORD from the environment.

# post a rant
DEVRANT_USERNAME=you DEVRANT_PASSWORD=secret6 \
  python examples/devrant/post_rant.py "Posted from a script" "python"

# run the full conformance test against a running server
DEVRANT_BASE=https://devplace.net python examples/devrant/smoke_test.py
DEVRANT_BASE=https://devplace.net node examples/devrant/smoke_test.mjs