DevPlace API Conventions, Content Negotiation and Error Envelope
Rules shared by every DevPlace endpoint: base URL, form vs JSON request bodies, HTML/JSON content negotiation, the action response envelope, cursor pagination, rate limits and the status-code and error-shape contract.
Shared rules that apply to every endpoint in this reference.
Base URL
Every example uses your current host:
https://devplace.net
Authentication
Each endpoint is tagged public, user, or admin. Authenticate user and admin endpoints with any of the four methods in Authentication: the session cookie, an X-API-KEY header, a Bearer token, or HTTP Basic credentials. The interactive panels on this site pre-fill your own API key, so you can run user-level calls immediately.
Request bodies
POST endpoints accept application/x-www-form-urlencoded form fields (the same fields the website submits). File uploads use multipart/form-data. A small number of endpoints accept a JSON body; those are noted explicitly.
HTML or JSON (content negotiation)
Every endpoint that renders a page or returns a redirect can also answer in JSON - the website keeps working exactly as before, and automation gets structured data from the same URLs. A request is served JSON when it sends either of:
Accept: application/jsonContent-Type: application/json
A normal browser navigation (Accept: text/html) always receives HTML, so nothing existing changes. Responses are defined by Pydantic models, so each page returns the same data the template renders.
Page reads (GET) return the page payload as a JSON object (lists include a next_cursor for pagination; detail pages embed author, comments, reactions, poll, and attachments).
Actions (the form POSTs: create / edit / delete / follow / send / mark-read …) return a uniform envelope instead of a 302 redirect:
{ "ok": true, "redirect": "/posts/abc-my-post", "data": { "uid": "…", "slug": "…", "url": "…" } }
data carries the created/affected resource where applicable, or null. Cookies (e.g. the session set on login/signup) are still set on JSON responses.
Errors are JSON too when JSON is requested:
{ "error": { "status": 404, "message": "Not found" } }
Validation failures return 422 with { "error": "validation", "fields": { "field": ["msg"] } }. Unauthenticated JSON requests to a protected endpoint return 401 (browsers are redirected to the login page instead); non-admins calling an admin endpoint get 403.
Trying it here
Every endpoint below has a live panel. Pick the response format (JSON by default, or HTML where the endpoint negotiates) and the panel sets the matching Accept header on the request and the generated cURL/JavaScript/Python snippets. The Expected tab always shows the modeled response shape; the Live response tab shows the real result after you press Send request.
AJAX responses (legacy shape)
The Votes, Reactions, Bookmarks & Polls endpoints predate the envelope and keep their original flat JSON shapes (e.g. { "saved": true }). They return JSON when the request carries an X-Requested-With: fetch header (a subset of the rule above); without it they 302 redirect, mirroring the browser flow. The interactive panels send the header for you.
Pagination
Most list endpoints page with an opaque cursor. Pass the before query parameter set to the createdat (or syncedat) value of the last item you received to fetch the next page; the JSON payload returns a next_cursor to use as the next before. This covers the feed, the post/project/gist/news lists, notifications, and saved bookmarks.
The follower and following lists are the exception: GET /profile/{username}/followers and GET /profile/{username}/following use classic page-based pagination via the page query parameter (25 per page), not a cursor.
Identifiers
Posts, projects, gists, and news articles accept either their slug or their bare UUID in the path. Slugs embed the first eight characters of the UUID.
Dates
All dates rendered to users are DD/MM/YYYY. Timestamps in stored records are ISO-8601 UTC.
Status codes
| Code | Meaning |
|---|---|
200 | Success (JSON or HTML) |
201 | Resource created (uploads) |
302 | Redirect (browser-style success for form posts) |
400 | Invalid request body or parameters |
401 | Credentials supplied but invalid |
403 | Authenticated but not allowed |
404 | Resource not found |
413 | Upload exceeds the configured size limit (see Uploads) |
415 | Upload file type not allowed (see Uploads) |
422 | Form/body validation failed (JSON clients) |
429 | Rate limit exceeded (see Rate limiting) |
503 | Maintenance mode |
Rate limiting
Mutating requests (POST/PUT/DELETE/PATCH) are rate limited per client IP over a rolling window; reads are not limited. The limit and window are configurable by an administrator (defaults: 60 requests per 60 seconds). When you exceed the limit you receive a 429 whose Retry-After header gives the number of seconds to wait before retrying. The OpenAI gateway (/openai/...) is exempt.
Troubleshooting
I get HTML back instead of JSON. Send Accept: application/json (or Content-Type: application/json on a body). A request is only served JSON when it asks for it and does not also accept text/html; a normal browser navigation always gets HTML. X-Requested-With: fetch is not a general JSON switch - it only applies to the legacy engagement actions (votes, reactions, bookmarks, polls).
An action returns 302 instead of the JSON envelope. Same cause: the request did not ask for JSON. Add the Accept: application/json header and the action returns { "ok": true, "redirect": "...", "data": {...} } instead of redirecting.
A protected endpoint redirects me to the login page. Browser-style (HTML) requests to a user/admin endpoint without valid credentials are redirected to login; the same request with Accept: application/json returns 401 instead. Non-admins calling an admin endpoint get 403 (JSON) or a redirect to the feed (HTML).
An upload is rejected with 413 or 415. 413 means the file exceeds the configured size limit; 415 means the file type is not in the allowed list. Both limits are set by an administrator.