REST API documentation
The Stackness REST API provides programmatic access to the platform. Browse 135 endpoints across 21 categories covering authentication, tools, stacks, moves, feeds, and discovery.
Base URL
https://stackness.dev/api/v1All endpoint paths below are relative to this base URL.
Authentication
Stackness supports two authentication methods:
Obtain a token via POST /auth/login or POST /auth/register. Pass it in the Authorization header:
Authorization: Bearer eyJhbGciOiJI...Create an API key (prefixed with sfk_) via the API keys endpoints or in settings. Pass it in the Authorization header:
Authorization: Bearer sfk_abc123...OAuth flows (GitHub, Google) are also available - see the authentication section below for redirect-based endpoints.
Pagination
List endpoints use cursor-based pagination. Pass cursor and limit as query parameters.
GET /api/v1/tools?limit=20&cursor=abc123
{
"tools": [...],
"next_cursor": "def456",
"has_more": true
}When has_more is false, you have reached the end of the list.
Error format
All errors return a JSON object with error and code fields:
{
"error": "resource not found",
"code": "NOT_FOUND"
}
Common HTTP status codes:
400 - Bad Request (validation error)
401 - Unauthorized (missing or invalid token)
403 - Forbidden (insufficient permissions)
404 - Not Found
409 - Conflict (duplicate resource)
429 - Too Many Requests (rate limited)
500 - Internal Server ErrorRate limits
| Tier | Limit | Scope |
|---|---|---|
| Public endpoints | 900 req/min | Per IP address |
| Authenticated endpoints | 600 req/min | Per user |
| Auth endpoints (login, register) | 10 req/min | Per IP address |
| Session endpoints (refresh, logout) | 60 req/min | Per IP address |
When rate limited, the API returns HTTP 429 with a Retry-After header.
Authentication
11Register, log in, refresh tokens, and authenticate via OAuth providers.
Create a new user account with email and password. Returns access and refresh tokens.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| string | Yes | Email address | |
| password | string | Yes | Password (min 8 chars, 1 uppercase, 1 number, 1 special) |
| username | string | Yes | Unique username (3-30 chars, alphanumeric + hyphens) |
| display_name | string | No | Display name |
Response
{
"user": {
"id": 1,
"username": "janedoe",
"email": "jane@example.com",
"display_name": "Jane Doe"
},
"access_token": "eyJhbGciOiJI...",
"refresh_token": "eyJhbGciOiJI..."
}curl -X POST https://stackness.dev/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"password": "SecurePass1!",
"username": "janedoe"
}'const res = await fetch("https://stackness.dev/api/v1/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "jane@example.com",
password: "SecurePass1!",
username: "janedoe",
}),
});
const data = await res.json();Authenticate with email and password. Returns JWT access and refresh tokens.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| string | Yes | Email address | |
| password | string | Yes | Account password |
Response
{
"user": {
"id": 1,
"username": "janedoe",
"email": "jane@example.com"
},
"access_token": "eyJhbGciOiJI...",
"refresh_token": "eyJhbGciOiJI..."
}curl -X POST https://stackness.dev/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com", "password": "SecurePass1!"}'const res = await fetch("https://stackness.dev/api/v1/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "jane@example.com",
password: "SecurePass1!",
}),
});
const data = await res.json();Exchange a valid refresh token for a new access token. The old refresh token is invalidated.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| refresh_token | string | Yes | Current refresh token |
Response
{
"access_token": "eyJhbGciOiJI...",
"refresh_token": "eyJhbGciOiJI..."
}curl -X POST https://stackness.dev/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "eyJhbGciOiJI..."}'const res = await fetch("https://stackness.dev/api/v1/auth/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});
const data = await res.json();Invalidate the current refresh token, effectively logging the user out.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| refresh_token | string | Yes | Refresh token to invalidate |
Response
{
"message": "logged out"
}curl -X POST https://stackness.dev/api/v1/auth/logout \
-H "Content-Type: application/json" \
-d '{"refresh_token": "eyJhbGciOiJI..."}'await fetch("https://stackness.dev/api/v1/auth/logout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});Redirects the user to GitHub for authorization. After consent, GitHub redirects back to the callback URL.
Response
302 Redirect to GitHub authorization page# Open in browser - this endpoint redirects
curl -v https://stackness.dev/api/v1/auth/github// Redirect the user in the browser
window.location.href = "https://stackness.dev/api/v1/auth/github";Handles the OAuth callback from GitHub. Exchanges the authorization code for tokens and creates or links the user account.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| code | string | Yes | Authorization code from GitHub (query param) |
| state | string | Yes | CSRF state token (query param) |
Response
302 Redirect to frontend with tokens# This endpoint is called by GitHub, not directly
# GitHub redirects to: /api/v1/auth/github/callback?code=...&state=...// This callback is handled automatically by the OAuth flow
// After redirect, tokens are available via query paramsRedirects the user to Google for authorization. After consent, Google redirects back to the callback URL.
Response
302 Redirect to Google authorization page# Open in browser - this endpoint redirects
curl -v https://stackness.dev/api/v1/auth/google// Redirect the user in the browser
window.location.href = "https://stackness.dev/api/v1/auth/google";Handles the OAuth callback from Google. Exchanges the authorization code for tokens and creates or links the user account.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| code | string | Yes | Authorization code from Google (query param) |
| state | string | Yes | CSRF state token (query param) |
Response
302 Redirect to frontend with tokens# This endpoint is called by Google, not directly
# Google redirects to: /api/v1/auth/google/callback?code=...&state=...// This callback is handled automatically by the OAuth flow
// After redirect, tokens are available via query paramsEmail a single-use password reset link to the given address. The response is the same generic 200 whether or not an account exists for that email, so the endpoint cannot be used to probe which emails are registered. OAuth-only accounts can complete the flow to set a password alongside their provider login.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| string | Yes | Email address of the account | |
| cf_turnstile_token | string | No | Cloudflare Turnstile captcha token, verified when captcha is enabled |
Response
{
"message": "If an account exists for that email, a password reset link has been sent."
}curl -X POST https://stackness.dev/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com"}'const res = await fetch("https://stackness.dev/api/v1/auth/forgot-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "jane@example.com" }),
});
const data = await res.json();Set a new password using the single-use token from a password reset email. On success all of the user's existing sessions are revoked, so every device must log in again. An invalid or expired token returns 400.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | Single-use reset token from the emailed link |
| password | string | Yes | New password (min 8 chars) |
Response
{
"message": "password has been reset"
}curl -X POST https://stackness.dev/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"token": "a1b2c3d4...", "password": "NewSecurePass1!"}'const res = await fetch("https://stackness.dev/api/v1/auth/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: resetToken, password: "NewSecurePass1!" }),
});
const data = await res.json();Swap the single-use code issued by a GitHub or Google OAuth callback for tokens. The access token is returned in the body and the refresh token is set as an HttpOnly cookie. Used by the OAuth callback page after the provider redirect; an invalid or expired code returns 401.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| code | string | Yes | Single-use code from the OAuth callback redirect |
Response
{
"access_token": "eyJhbGciOiJI..."
}curl -X POST https://stackness.dev/api/v1/auth/oauth/exchange \
-H "Content-Type: application/json" \
-d '{"code": "a1b2c3d4..."}'const res = await fetch("https://stackness.dev/api/v1/auth/oauth/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code }),
});
const data = await res.json();API keys
3Create and manage API keys for programmatic access and MCP server authentication.
Create an API key for programmatic access. The key (prefixed with "sfk_") is only returned once in the response.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Descriptive name for the key |
Response
{
"id": 1,
"name": "Claude Desktop",
"key": "sfk_abc123...",
"created_at": "2025-01-15T10:30:00Z"
}curl -X POST https://stackness.dev/api/v1/api-keys \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Claude Desktop"}'const res = await fetch("https://stackness.dev/api/v1/api-keys", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Claude Desktop" }),
});
const data = await res.json();List all API keys for the authenticated user. Key values are not included for security.
Response
{
"api_keys": [
{
"id": 1,
"name": "Claude Desktop",
"prefix": "sfk_abc1",
"created_at": "2025-01-15T10:30:00Z",
"last_used_at": "2025-01-16T08:00:00Z"
}
]
}curl https://stackness.dev/api/v1/api-keys \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/api-keys", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Permanently revoke and delete an API key. Any requests using this key will immediately fail.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | API key ID (path param) |
Response
{
"message": "api key deleted"
}curl -X DELETE https://stackness.dev/api/v1/api-keys/1 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/api-keys/1", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});Users & profiles
8View and update user profiles, get followers/following lists, OG images, and QR codes.
Return the full profile of the currently authenticated user, including email and settings.
Response
{
"id": 1,
"username": "janedoe",
"email": "jane@example.com",
"display_name": "Jane Doe",
"bio": "Full-stack developer",
"job_title": "Senior frontend engineer",
"avatar_url": "https://cdn.stackness.dev/avatars/1.jpg",
"is_pro": false,
"onboarding_completed": true,
"created_at": "2025-01-01T00:00:00Z"
}curl https://stackness.dev/api/v1/users/me \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/users/me", {
headers: { Authorization: `Bearer ${token}` },
});
const user = await res.json();Update the authenticated user's profile fields. Only provided fields are changed.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| display_name | string | No | Display name |
| bio | string | No | Short biography |
| job_title | string | No | Job title shown on the profile and blog byline |
| website | string | No | Personal website URL |
| location | string | No | Location |
| company | string | No | Company name |
Response
{
"id": 1,
"username": "janedoe",
"display_name": "Jane Doe",
"bio": "Updated bio",
"website": "https://jane.dev"
}curl -X PATCH https://stackness.dev/api/v1/users/me \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"bio": "Updated bio", "website": "https://jane.dev"}'const res = await fetch("https://stackness.dev/api/v1/users/me", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ bio: "Updated bio" }),
});
const user = await res.json();Record that the authenticated user has finished or skipped the onboarding walkthrough. Sets the completion timestamp on the first call and leaves it untouched afterwards, so retries are safe. The flag is returned as onboarding_completed on GET /api/v1/users/me and on the user object embedded in auth responses.
Response
204 No Contentcurl -X POST https://stackness.dev/api/v1/users/me/onboarding/complete \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/users/me/onboarding/complete", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});Retrieve a user's public profile by username. Does not require authentication.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
Response
{
"id": 1,
"username": "janedoe",
"display_name": "Jane Doe",
"bio": "Full-stack developer",
"job_title": "Senior frontend engineer",
"avatar_url": "https://cdn.stackness.dev/avatars/1.jpg",
"followers_count": 42,
"following_count": 18,
"stack_count": 12,
"is_pro": true
}curl https://stackness.dev/api/v1/users/janedoeconst res = await fetch("https://stackness.dev/api/v1/users/janedoe");
const profile = await res.json();List the followers of a given user. Supports cursor-based pagination.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 100) |
Response
{
"followers": [
{
"id": 2,
"username": "johndoe",
"display_name": "John Doe",
"avatar_url": "https://cdn.stackness.dev/avatars/2.jpg"
}
],
"next_cursor": "abc123",
"has_more": true
}curl "https://stackness.dev/api/v1/users/janedoe/followers?limit=20"const res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/followers?limit=20"
);
const data = await res.json();List the users that a given user is following. Supports cursor-based pagination.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 100) |
Response
{
"following": [
{
"id": 3,
"username": "alice",
"display_name": "Alice",
"avatar_url": "https://cdn.stackness.dev/avatars/3.jpg"
}
],
"next_cursor": "def456",
"has_more": false
}curl "https://stackness.dev/api/v1/users/janedoe/following?limit=20"const res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/following?limit=20"
);
const data = await res.json();Generate or retrieve a cached Open Graph image for the user's profile. Returns a PNG image.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
Response
Binary PNG image (Content-Type: image/png)curl -o og.png https://stackness.dev/api/v1/users/janedoe/og-imageconst res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/og-image"
);
const blob = await res.blob();Generate a QR code that links to the user's profile. Returns a PNG image.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
Response
Binary PNG image (Content-Type: image/png)curl -o qr.png https://stackness.dev/api/v1/users/janedoe/qr-codeconst res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/qr-code"
);
const blob = await res.blob();Follow system
3Follow and unfollow users, and check follow status.
Start following the specified user. You will see their activity in your feed.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username to follow (path param) |
Response
{
"message": "followed",
"following": true
}curl -X POST https://stackness.dev/api/v1/users/janedoe/follow \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/users/janedoe/follow", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});Stop following the specified user. Their activity will no longer appear in your feed.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username to unfollow (path param) |
Response
{
"message": "unfollowed",
"following": false
}curl -X DELETE https://stackness.dev/api/v1/users/janedoe/follow \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/users/janedoe/follow", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});Check whether the authenticated user is following the specified user.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username to check (path param) |
Response
{
"is_following": true
}curl https://stackness.dev/api/v1/users/janedoe/is-following \
-H "Authorization: Bearer <token>"const res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/is-following",
{ headers: { Authorization: `Bearer ${token}` } }
);
const data = await res.json();Tools & categories
13Browse the tool database, search for tools, view usage stats, and submit new tools.
Get the full list of tool categories (e.g., Databases, Frontend Frameworks, DevOps).
Response
{
"categories": [
{
"id": 1,
"name": "Databases",
"slug": "databases",
"display_order": 1,
"description": "Where developers keep state...",
"tool_count": 24
}
]
}curl https://stackness.dev/api/v1/categoriesconst res = await fetch("https://stackness.dev/api/v1/categories");
const data = await res.json();Everything behind one category hub page: the category with its intro, the tools ranked by how many people have them in a stack (the top ten carry twelve weeks of trend points), the risers and fallers of the last thirty days, the tools most often paired from outside the category, the most popular moves, the slugs whose imported history the page charts, and up to six eligible comparisons between the category's own tools. Risers and fallers are omitted when fewer than three tools qualify. `indexable` is false for a thin category, which renders a short page and stays out of the sitemap. The whole aggregate is cached for an hour.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Category slug (path param), e.g. terminal-shell |
Response
{
"category": {
"id": 2,
"name": "Terminal & Shell",
"slug": "terminal-shell",
"display_order": 2,
"description": "Where developers keep state...",
"tool_count": 24
},
"indexable": true,
"data_as_of": "2026-09-08T09:14:02Z",
"tools": [
{
"id": 12,
"name": "Ghostty",
"slug": "ghostty",
"description": "GPU-accelerated terminal emulator",
"logo_url": "https://cdn.stackness.dev/logos/ghostty.png",
"user_count": 184,
"categories": [{ "id": 2, "name": "Terminal & Shell", "slug": "terminal-shell", "display_order": 2 }],
"created_at": "2025-04-02T10:00:00Z",
"trend": [{ "week": "2026-06-22", "user_count": 151 }]
}
],
"risers": [
{ "tool_id": 12, "name": "Ghostty", "slug": "ghostty", "count": 33, "total_users": 184, "rate": 17.9 }
],
"fallers": [],
"pairings": [
{ "tool_id": 45, "name": "Neovim", "slug": "neovim", "count": 6, "user_count": 402 }
],
"moves": [
{
"id": 88,
"slug": "switched-to-ghostty",
"title": "Switched to Ghostty",
"tools": [{ "id": 12, "name": "Ghostty", "slug": "ghostty" }],
"is_backdated": false,
"author": { "id": 3, "username": "sarah_chen", "is_pro": true },
"reaction_count": 12,
"comment_count": 4,
"created_at": "2026-05-11T08:00:00Z",
"updated_at": "2026-05-11T08:00:00Z"
}
],
"history_slugs": ["ghostty", "zsh", "fish"],
"comparisons": [
{
"slug": "fish-vs-zsh",
"tool_a": { "name": "Fish", "slug": "fish" },
"tool_b": { "name": "Zsh", "slug": "zsh" }
}
]
}curl https://stackness.dev/api/v1/categories/terminal-shellconst res = await fetch(
"https://stackness.dev/api/v1/categories/terminal-shell"
);
const data = await res.json();List tools with optional filtering by category. Supports cursor-based pagination.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| category | string | No | Filter by category slug |
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 100) |
Response
{
"tools": [
{
"id": 1,
"name": "PostgreSQL",
"slug": "postgresql",
"description": "Open-source relational database",
"website": "https://postgresql.org",
"category": "Databases",
"user_count": 340
}
],
"next_cursor": "abc123",
"has_more": true
}curl "https://stackness.dev/api/v1/tools?category=databases&limit=20"const res = await fetch(
"https://stackness.dev/api/v1/tools?category=databases&limit=20"
);
const data = await res.json();Full-text search across tool names and descriptions.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | Search query |
| limit | integer | No | Max results (default 20, max 100) |
Response
{
"tools": [
{
"id": 1,
"name": "PostgreSQL",
"slug": "postgresql",
"description": "Open-source relational database",
"user_count": 340
}
]
}curl "https://stackness.dev/api/v1/tools/search?q=postgres"const res = await fetch(
"https://stackness.dev/api/v1/tools/search?q=postgres"
);
const data = await res.json();Get full details for a specific tool including description, website, category, and usage stats.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Tool slug (path param) |
Response
{
"id": 1,
"name": "PostgreSQL",
"slug": "postgresql",
"description": "Open-source relational database",
"website": "https://postgresql.org",
"category": {
"id": 1,
"name": "Databases",
"slug": "databases"
},
"user_count": 340,
"created_at": "2025-01-01T00:00:00Z"
}curl https://stackness.dev/api/v1/tools/postgresqlconst res = await fetch("https://stackness.dev/api/v1/tools/postgresql");
const tool = await res.json();List users who have this tool in their stack. Supports cursor-based pagination.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Tool slug (path param) |
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 100) |
Response
{
"users": [
{
"id": 1,
"username": "janedoe",
"display_name": "Jane Doe",
"avatar_url": "https://cdn.stackness.dev/avatars/1.jpg"
}
],
"next_cursor": "abc123",
"has_more": true
}curl "https://stackness.dev/api/v1/tools/postgresql/users?limit=10"const res = await fetch(
"https://stackness.dev/api/v1/tools/postgresql/users?limit=10"
);
const data = await res.json();Power a tool picker: given the slugs a user has already selected, returns tools commonly used alongside them, scored by how many of the selected tools each candidate is paired with. With no selection it returns trending tools. The list is topped up with popular tools from the selection's categories so it is never empty, and excludes the selected tools plus, when authenticated, everything already in your stack.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| selected | string | No | Comma-separated tool slugs already selected (max 50) |
| category | string | No | Category slug to prefer when topping up with popular tools |
| limit | integer | No | Max results (default 24, max 60) |
Response
{
"tools": [
{
"tool_id": 5,
"name": "Redis",
"slug": "redis",
"logo_url": "https://cdn.stackness.dev/logos/redis.png",
"description": "In-memory data store",
"category": { "id": 2, "name": "Databases", "slug": "databases" },
"reason": "used_with:postgresql",
"score": 180
}
]
}curl "https://stackness.dev/api/v1/tools/suggestions?selected=postgresql,go&limit=24"const res = await fetch(
"https://stackness.dev/api/v1/tools/suggestions?selected=postgresql,go&limit=24"
);
const data = await res.json();Get tools commonly used alongside this one, based on stack co-occurrence analysis. Each peer carries its categories so a caller can tell which of them sit in the same category as the tool being viewed, and `compare_eligible` is true when the two tools have enough shared data for a comparison page at /compare/{a}-vs-{b}.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Tool slug (path param) |
| limit | integer | No | Max results (default 10) |
Response
{
"tools": [
{
"tool_id": 5,
"name": "Redis",
"slug": "redis",
"logo_url": "https://cdn.stackness.dev/logos/redis.png",
"user_count": 180,
"categories": [
{ "id": 3, "name": "Databases", "slug": "databases" }
],
"compare_eligible": false
}
]
}curl "https://stackness.dev/api/v1/tools/postgresql/also-used-with"const res = await fetch(
"https://stackness.dev/api/v1/tools/postgresql/also-used-with"
);
const data = await res.json();Get the adoption trend for a tool over time (weekly data points).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Tool slug (path param) |
Response
{
"tool": "postgresql",
"data_points": [
{ "date": "2025-01-06", "user_count": 300 },
{ "date": "2025-01-13", "user_count": 320 },
{ "date": "2025-01-20", "user_count": 340 }
]
}curl https://stackness.dev/api/v1/tools/postgresql/trendconst res = await fetch(
"https://stackness.dev/api/v1/tools/postgresql/trend"
);
const data = await res.json();Submit a tool to be added to the database. Submitted tools are reviewed before becoming public.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Tool name |
| website_url | string | No | Tool website URL |
| description | string | No | Short description |
| logo_url | string | No | Logo image URL |
| category_ids | integer[] | No | Category IDs the tool belongs to |
Response
{
"id": 500,
"name": "My New Tool",
"slug": "my-new-tool",
"status": "pending",
"created_at": "2025-01-20T10:00:00Z"
}curl -X POST https://stackness.dev/api/v1/tools \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "My New Tool", "website_url": "https://example.com"}'const res = await fetch("https://stackness.dev/api/v1/tools", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "My New Tool",
website_url: "https://example.com",
}),
});
const data = await res.json();Get the activity feed for a specific tool (moves, stack additions, etc.).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Tool slug (path param) |
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 50) |
Response
{
"items": [
{
"id": "feed_123",
"type": "stack_add",
"user": { "username": "janedoe", "display_name": "Jane Doe" },
"tool": { "name": "PostgreSQL", "slug": "postgresql" },
"created_at": "2025-01-20T10:00:00Z"
}
],
"next_cursor": "abc123",
"has_more": true
}curl "https://stackness.dev/api/v1/tools/postgresql/feed?limit=10"const res = await fetch(
"https://stackness.dev/api/v1/tools/postgresql/feed?limit=10"
);
const data = await res.json();Get the imported popularity history for a tool as labelled time series, one per source, metric, and granularity combination. Values come from external sources in each source's own unit, so series are displayed separately and never summed. Each point's bucket is the start date of the month or year it covers. A tool with no imported history returns an empty series list; an unknown source or metric returns 400.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Tool slug (path param) |
| source | string | No | Comma-separated source filter: so_survey, sede_tags, wiki_pageviews, wikidata, npm, hn_mentions, homebrew, gharchive, survey_js, survey_jetbrains, redmonk, google_trends, pypi, wayback, manual (query param) |
| metric | string | No | Comma-separated metric filter: usage_share_pct, satisfaction_pct, question_count, pageviews, downloads, mentions, installs, stars, rank, search_interest (query param) |
Response
{
"series": [
{
"source": "so_survey",
"source_label": "Stack Overflow developer survey",
"source_url": "https://survey.stackoverflow.co/2024",
"metric": "usage_share_pct",
"metric_label": "Usage share",
"unit": "% of respondents",
"granularity": "year",
"confidence": "high",
"origin": "imported",
"points": [
{ "bucket": "2023-01-01", "value": 45.6, "is_estimated": false },
{ "bucket": "2024-01-01", "value": 48.7, "is_estimated": false }
]
}
]
}curl "https://stackness.dev/api/v1/tools/postgresql/history?source=so_survey&metric=usage_share_pct"const res = await fetch(
"https://stackness.dev/api/v1/tools/postgresql/history?source=so_survey"
);
const data = await res.json();Everything behind a /compare/{a}-vs-{b} page: both tools with their user counts, twelve weeks of trend points and the net change, their imported history series, their three most popular public moves, how many members list both, how many moved from one to the other in each direction and the months those moves span, the pairings they share and the ones only each has, and the curated verdict when one is published. The slugs may come in either order; `pair`, `tool_a` and `tool_b` always carry the canonical alphabetical order, so a caller can redirect to it. A page exists only for pairs whose data clears the thresholds (shared category, minimum users and imported series per tool, enough overlap). An unknown or unapproved tool returns 404 with code `NOT_FOUND`; a pair that does not qualify returns 404 with code `NOT_ELIGIBLE`. `noindex` is true while the comparison tree is switched out of search indexes.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| a | string | Yes | First tool slug (path param), e.g. vs-code |
| b | string | Yes | Second tool slug (path param), e.g. neovim |
Response
{
"pair": "neovim-vs-vs-code",
"tool_a": {
"id": 45,
"name": "Neovim",
"slug": "neovim",
"description": "Hyperextensible Vim-based text editor",
"website_url": "https://neovim.io",
"logo_url": "https://cdn.stackness.dev/logos/neovim.png",
"is_approved": true,
"user_count": 402,
"categories": [{ "id": 1, "name": "Editor & IDE", "slug": "editor-ide", "display_order": 1 }],
"created_at": "2025-01-10T09:00:00Z",
"trend": [
{ "week": "2026-06-22", "user_count": 371 },
{ "week": "2026-09-07", "user_count": 402 }
],
"trend_delta": 31,
"history": [
{
"source": "so_survey",
"source_label": "Stack Overflow developer survey",
"source_url": "https://survey.stackoverflow.co",
"metric": "usage_share_pct",
"metric_label": "Usage share",
"unit": "% of respondents",
"granularity": "year",
"confidence": "high",
"origin": "imported",
"points": [
{ "bucket": "2024-01-01", "value": 12.5, "is_estimated": false },
{ "bucket": "2025-01-01", "value": 14.1, "is_estimated": false }
]
}
],
"moves": [
{
"id": 88,
"slug": "moved-my-config-to-lazy-nvim",
"title": "Moved my config to lazy.nvim",
"tools": [{ "id": 45, "name": "Neovim", "slug": "neovim" }],
"is_backdated": false,
"author": { "id": 3, "username": "sarah_chen", "is_pro": true },
"reaction_count": 12,
"comment_count": 4,
"created_at": "2026-05-11T08:00:00Z",
"updated_at": "2026-05-11T08:00:00Z"
}
]
},
"tool_b": {
"id": 12,
"name": "VS Code",
"slug": "vs-code",
"is_approved": true,
"user_count": 1204,
"categories": [{ "id": 1, "name": "Editor & IDE", "slug": "editor-ide", "display_order": 1 }],
"created_at": "2024-11-02T09:00:00Z",
"trend": [
{ "week": "2026-06-22", "user_count": 1188 },
{ "week": "2026-09-07", "user_count": 1204 }
],
"trend_delta": 16,
"history": [],
"moves": []
},
"shared_users": 96,
"switched_a_to_b": 7,
"switched_b_to_a": 23,
"switch_period": { "from": "2023-02", "to": "2026-08" },
"pairings": {
"shared": [
{ "tool_id": 5, "name": "Git", "slug": "git", "users_with_a": 210, "users_with_b": 640 }
],
"only_a": [
{ "tool_id": 61, "name": "tmux", "slug": "tmux", "users_with_a": 118, "users_with_b": 0 }
],
"only_b": [
{ "tool_id": 77, "name": "GitHub Copilot", "slug": "github-copilot", "users_with_a": 0, "users_with_b": 301 }
]
},
"data_as_of": "2026-09-07T18:02:11Z",
"verdict": {
"body": "Neovim suits people who already live in the terminal...",
"author": { "username": "gordeychuk_s", "display_name": "Sergei Gordeichuk" },
"updated_at": "2026-09-05T10:00:00Z"
},
"noindex": false
}curl https://stackness.dev/api/v1/compare/vs-code/neovimconst res = await fetch(
"https://stackness.dev/api/v1/compare/vs-code/neovim"
);
if (res.status === 404) {
const { code } = await res.json(); // "NOT_FOUND" or "NOT_ELIGIBLE"
}
const data = await res.json();Stack management
18Manage your developer tool stack: add, update, and remove tools. View public stacks and stack history.
Add a tool to your personal stack with an optional note about how you use it.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| tool_id | integer | Yes | ID of the tool to add |
| personal_note | string | No | Personal note about why/how you use this tool |
| category_id | integer | No | Category ID (see GET /categories); omitted means Uncategorized |
| start_date | string | No | Month you started using the tool, RFC3339. Month granularity: the day is ignored and the date is stored as the first of the month |
| end_date | string | No | Month you stopped using the tool, RFC3339. Month granularity: the day is ignored |
| visibility | string | No | "public" (default), "friends", "private", or "subscribers" |
| screenshot_url | string | No | Screenshot URL for this entry |
Response
{
"id": 1,
"tool": {
"id": 1,
"name": "PostgreSQL",
"slug": "postgresql"
},
"personal_note": "Primary database for all projects",
"visibility": "public",
"created_at": "2025-01-20T10:00:00Z"
}curl -X POST https://stackness.dev/api/v1/stack/tools \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"tool_id": 1, "personal_note": "Primary database"}'const res = await fetch("https://stackness.dev/api/v1/stack/tools", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ tool_id: 1, personal_note: "Primary database" }),
});
const data = await res.json();Add up to 50 tools in one transaction. Tools already in your stack are skipped and reported instead of failing the call. Public adds publish a single grouped stack update so followers see one card, not one per tool. Each entry defaults to the tool's first catalog category unless category_id is given.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| tools | array | Yes | 1-50 entries of { tool_id: integer, category_id?: integer }. Duplicate tool_ids are rejected |
| visibility | string | No | public (default), friends, or subscribers (subscribers requires a Supporter subscription). Applies to every entry |
| note | string | No | Optional note shown on the grouped stack update (max 2000 chars) |
Response
{
"added": [
{
"id": 12,
"tool": { "id": 1, "name": "PostgreSQL", "slug": "postgresql" },
"visibility": "public",
"created_at": "2025-01-20T10:00:00Z"
}
],
"skipped": [{ "tool_id": 5, "reason": "already_in_stack" }],
"stack_update_id": 42
}curl -X POST https://stackness.dev/api/v1/stack/tools/bulk \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"tools": [{"tool_id": 1}, {"tool_id": 5}], "note": "My starter kit"}'const res = await fetch("https://stackness.dev/api/v1/stack/tools/bulk", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ tools: [{ tool_id: 1 }, { tool_id: 5 }], note: "My starter kit" }),
});
const data = await res.json();List all tools in the authenticated user's stack, grouped by category.
Response
{
"tools": [
{
"id": 1,
"tool": {
"id": 1,
"name": "PostgreSQL",
"slug": "postgresql",
"category": "Databases"
},
"note": "Primary database",
"added_at": "2025-01-20T10:00:00Z"
}
],
"total": 12
}curl https://stackness.dev/api/v1/stack/tools \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/stack/tools", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Update the note, category, dates, or visibility of a tool in your stack.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Stack entry ID (path param) |
| personal_note | string | No | Updated note |
| category_id | integer | No | Category ID (see GET /categories) |
| start_date | string | No | Month you started using the tool, RFC3339. Month granularity: the day is ignored and the date is stored as the first of the month |
| end_date | string | No | Month you stopped using the tool, RFC3339. Month granularity: the day is ignored |
| visibility | string | No | "public", "friends", "private", or "subscribers" |
| screenshot_url | string | No | Screenshot URL for this entry |
Response
{
"id": 1,
"tool": {
"id": 1,
"name": "PostgreSQL",
"slug": "postgresql"
},
"personal_note": "Updated note",
"visibility": "public",
"updated_at": "2025-01-20T10:00:00Z"
}curl -X PATCH https://stackness.dev/api/v1/stack/tools/1 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"personal_note": "Updated note"}'const res = await fetch("https://stackness.dev/api/v1/stack/tools/1", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ personal_note: "Updated note" }),
});
const data = await res.json();Remove a tool from your stack. This does not delete the tool from the database.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Stack entry ID (path param) |
Response
{
"message": "tool removed from stack"
}curl -X DELETE https://stackness.dev/api/v1/stack/tools/1 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/stack/tools/1", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});View the public stack for any user by their username. The response carries the subspace grid (user-defined groups with ordered tools, moves, and clusters plus per-tile layout state) alongside the legacy category grouping.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
Response
{
"categories": [
{
"category": { "id": 2, "name": "Databases", "slug": "databases", "display_order": 1 },
"tools": [{ "id": 12, "tool": { "id": 1, "name": "PostgreSQL", "slug": "postgresql" } }]
}
],
"subspaces": [
{
"id": 4,
"name": "Daily drivers",
"slug": "daily-drivers",
"color": "indigo",
"position": 0,
"start_collapsed": false,
"width": "full",
"items": [
{
"type": "tool",
"position": 0,
"size": "featured",
"use_image_background": false,
"dim_image_background": false,
"user_tool": {
"id": 12,
"tool": { "id": 1, "name": "PostgreSQL", "slug": "postgresql" },
"personal_note": "Primary database",
"reactions": [{ "emoji": "fire", "count": 3, "reacted": false }],
"comment_count": 1
}
},
{
"type": "move",
"position": 1,
"size": "normal",
"use_image_background": false,
"dim_image_background": false,
"move": { "id": 7, "slug": "prompt-driven-tdd", "title": "Prompt-driven TDD", "reactions": [], "comment_count": 0 }
}
],
"clusters": [
{
"id": 2,
"name": "Little helpers",
"position": 2,
"items": [{ "type": "tool", "position": 0, "size": "mini", "cluster_id": 2, "user_tool": { "id": 15 } }]
}
]
}
],
"ungrouped": { "items": [], "clusters": [] }
}curl https://stackness.dev/api/v1/users/janedoe/stackconst res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/stack"
);
const data = await res.json();Create a named, colored subspace on your profile grid. The anchor slug is derived from the name once and stays stable across renames, so deep links keep working.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Subspace name, at most 50 characters |
| color | string | No | Palette color: indigo, sky, teal, green, amber, rose, violet, or slate. Defaults to indigo |
| start_collapsed | boolean | No | Whether visitors see this subspace collapsed at first |
Response
{
"id": 4,
"name": "Daily drivers",
"slug": "daily-drivers",
"color": "indigo",
"position": 3,
"start_collapsed": false,
"width": "full",
"created_at": "2026-08-25T12:00:00Z",
"updated_at": "2026-08-25T12:00:00Z"
}curl -X POST https://stackness.dev/api/v1/stack/subspaces \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Daily drivers", "color": "indigo"}'const res = await fetch("https://stackness.dev/api/v1/stack/subspaces", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Daily drivers", color: "indigo" }),
});Set the top-to-bottom order of your subspaces. Positions follow the order of the given ids.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| subspace_ids | number[] | Yes | All subspace ids in their new order |
Response
204 No Contentcurl -X PATCH https://stackness.dev/api/v1/stack/subspaces/reorder \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"subspace_ids": [4, 2, 7]}'await fetch("https://stackness.dev/api/v1/stack/subspaces/reorder", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ subspace_ids: [4, 2, 7] }),
});Rename or recolor a subspace, set whether it starts collapsed, or set its panel width. The anchor slug never changes on rename.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | Subspace ID (path param) |
| name | string | No | New name, at most 50 characters |
| color | string | No | New palette color |
| start_collapsed | boolean | No | Whether visitors see this subspace collapsed at first |
| width | string | No | Panel width on desktop: full or half. Two half-width subspaces sit side by side; on mobile they stack |
Response
{
"id": 4,
"name": "Weekend experiments",
"slug": "daily-drivers",
"color": "rose",
"position": 3,
"start_collapsed": false,
"width": "half",
"created_at": "2026-08-25T12:00:00Z",
"updated_at": "2026-08-25T12:30:00Z"
}curl -X PATCH https://stackness.dev/api/v1/stack/subspaces/4 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"color": "rose"}'await fetch("https://stackness.dev/api/v1/stack/subspaces/4", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ color: "rose" }),
});Delete a subspace. Its tiles and clusters are not removed - they fall back to the ungrouped grid at the end of the profile.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | Subspace ID (path param) |
Response
204 No Contentcurl -X DELETE https://stackness.dev/api/v1/stack/subspaces/4 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/stack/subspaces/4", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});Create a named cluster - a single tile that holds mini chips. Assign items to it through the layout endpoint.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Cluster name, at most 50 characters |
| subspace_id | number | No | Subspace the cluster tile lives in; omit for the ungrouped grid |
| position | number | No | Grid position of the cluster tile |
Response
{
"id": 2,
"name": "Little helpers",
"subspace_id": 4,
"position": 5,
"created_at": "2026-08-25T12:00:00Z",
"updated_at": "2026-08-25T12:00:00Z"
}curl -X POST https://stackness.dev/api/v1/stack/clusters \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Little helpers", "subspace_id": 4}'const res = await fetch("https://stackness.dev/api/v1/stack/clusters", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Little helpers", subspace_id: 4 }),
});Rename a cluster.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | Cluster ID (path param) |
| name | string | Yes | New name, at most 50 characters |
Response
{
"id": 2,
"name": "Python helpers",
"subspace_id": 4,
"position": 5,
"created_at": "2026-08-25T12:00:00Z",
"updated_at": "2026-08-25T12:45:00Z"
}curl -X PATCH https://stackness.dev/api/v1/stack/clusters/2 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Python helpers"}'await fetch("https://stackness.dev/api/v1/stack/clusters/2", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Python helpers" }),
});Ungroup a cluster: its mini chips are promoted back to small tiles in place, then the cluster tile is removed.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | number | Yes | Cluster ID (path param) |
Response
204 No Contentcurl -X DELETE https://stackness.dev/api/v1/stack/clusters/2 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/stack/clusters/2", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});Batch-save the profile grid arrangement: item positions, sizes, subspace and cluster membership, image background flags, and cluster placement in one call. Size is one of featured, wide, tall, normal, small, or mini; items inside a cluster must use size mini.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| items | object[] | Yes | Item placements: { type: 'tool' | 'move', id, subspace_id?, cluster_id?, position, size, use_image_background?, dim_image_background? } |
| clusters | object[] | No | Cluster placements: { id, subspace_id?, position } |
Response
204 No Contentcurl -X PUT https://stackness.dev/api/v1/stack/layout \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"items": [{"type": "tool", "id": 12, "subspace_id": 4, "position": 0, "size": "featured"}]}'await fetch("https://stackness.dev/api/v1/stack/layout", {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
items: [
{ type: "tool", id: 12, subspace_id: 4, position: 0, size: "featured" },
],
}),
});Publish a summary of recent changes to your stack as a feed item visible to your followers. Each change carries a change_type (tool_added, tool_removed, move_added, move_retired, subspace_created, subspace_renamed, subspace_removed, or layout_updated) and a target_type (user_tool, move, subspace, or stack).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| note | string | No | Optional note about what changed and why, at most 2000 characters |
| changes | array | Yes | 1-50 changes: { change_type, target_type, target_id, target_name, target_logo?, target_slug? } |
Response
{
"id": 1,
"user_id": 42,
"note": "Spring cleaning",
"changes": [
{ "change_type": "tool_added", "target_type": "user_tool", "target_id": 12, "target_name": "Bun" },
{ "change_type": "subspace_created", "target_type": "subspace", "target_id": 4, "target_name": "Daily drivers" },
{ "change_type": "layout_updated", "target_type": "stack", "target_id": 0, "target_name": "Stack layout" }
],
"published_at": "2025-01-20T10:00:00Z",
"created_at": "2025-01-20T10:00:00Z"
}curl -X POST https://stackness.dev/api/v1/stack/updates \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"note": "Spring cleaning", "changes": [{"change_type": "tool_added", "target_type": "user_tool", "target_id": 12, "target_name": "Bun"}]}'const res = await fetch("https://stackness.dev/api/v1/stack/updates", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
note: "Spring cleaning",
changes: [
{
change_type: "tool_added",
target_type: "user_tool",
target_id: 12,
target_name: "Bun",
},
],
}),
});
const data = await res.json();Copy one item from someone else's stack into your own. `target_type` picks what is being copied: `user_tool` copies a single stack entry (the tool, its category and its note), `move` copies a move together with the tools it references. Returns 201 with the item that was created. Copying a tool you already have returns 409 with code `TOOL_ALREADY_IN_STACK` - moves have no such guard and may be copied more than once. Copying your own item returns 403, and a source that was deleted or is no longer public returns 404.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| target_type | string | Yes | What to copy: "user_tool" or "move" |
| target_id | integer | Yes | ID of the stack entry or move to copy, as returned by the profile and move endpoints |
Response
{
"type": "user_tool",
"user_tool": {
"id": 812,
"tool": { "id": 10, "name": "Bun", "slug": "bun" },
"category": { "id": 3, "name": "Runtime", "slug": "runtime", "display_order": 2 },
"personal_note": "Faster installs than npm",
"visibility": "public",
"is_backdated": false,
"created_at": "2026-08-28T10:00:00Z",
"updated_at": "2026-08-28T10:00:00Z",
"reactions": [],
"comment_count": 0
}
}curl -X POST https://stackness.dev/api/v1/stack/copy \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"target_type": "user_tool", "target_id": 412}'const res = await fetch("https://stackness.dev/api/v1/stack/copy", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ target_type: "user_tool", target_id: 412 }),
});
if (res.status === 409) {
// code: "TOOL_ALREADY_IN_STACK" - the tool is already in your stack
}
const data = await res.json();Every dated stack entry for a user - tools and moves, active and retired - with category, dates, reactions and comment counts. Powers the stack-over-time chart, so the list is not paginated. The owner (authenticated as that user) sees private entries; every other viewer sees public-visibility entries only.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
Response
{
"items": [
{
"type": "user_tool",
"id": 12,
"tool": { "id": 1, "name": "PostgreSQL", "slug": "postgresql" },
"category": { "id": 2, "name": "Databases", "slug": "databases", "display_order": 1 },
"personal_note": "Primary database",
"start_date": "2023-04-01T00:00:00Z",
"end_date": "2025-01-01T00:00:00Z",
"comment_count": 3,
"reactions": [{ "emoji": "fire", "count": 2, "reacted": false }]
},
{
"type": "move",
"id": 7,
"title": "Prompt-driven TDD",
"start_date": "2024-06-01T00:00:00Z",
"comment_count": 0,
"reactions": []
}
]
}curl "https://stackness.dev/api/v1/users/janedoe/history"const res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/history"
);
const data = await res.json();Get a single published stack update by id, including its note and the changes it announced. Each change names what it touched: change_type is one of tool_added, tool_removed, move_added, move_retired, subspace_created, subspace_renamed, subspace_removed, or layout_updated, and target_type is user_tool, move, subspace, or stack.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Stack update id (path param) |
Response
{
"id": 42,
"user_id": 1,
"note": "Swapped my test runner and retired an old workflow.",
"changes": [
{
"change_type": "tool_added",
"target_type": "user_tool",
"target_id": 20,
"target_name": "Vitest",
"target_slug": "vitest"
},
{
"change_type": "move_retired",
"target_type": "move",
"target_id": 7,
"target_name": "Prompt-Driven TDD Workflow",
"target_slug": "prompt-driven-tdd-workflow"
}
],
"published_at": "2025-01-20T10:00:00Z",
"created_at": "2025-01-20T10:00:00Z"
}curl https://stackness.dev/api/v1/stack/updates/42 \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/stack/updates/42", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Moves
8Create and manage moves - techniques, workflows, and approaches for how you use your stack.
Create a new move - a technique, workflow, or approach you use. Moves appear in your followers' feeds.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| title | string | Yes | Move title |
| description | string | No | Detailed description of the move |
| tool_ids | integer[] | No | IDs of tools this move references |
| category_id | integer | No | Category ID (see GET /categories) |
| start_date | string | No | Month you started using the move, RFC3339. Month granularity: the day is ignored and the date is stored as the first of the month |
| end_date | string | No | Month you stopped using the move, RFC3339. Month granularity: the day is ignored |
| cover_image_url | string | No | Cover image URL |
| visibility | string | No | "public" (default), "friends", "private", or "subscribers" |
Response
{
"id": 1,
"slug": "prompt-driven-tdd-workflow",
"title": "Prompt-Driven TDD Workflow",
"description": "Write the test prompt first, then implement until green...",
"tools": [{ "name": "Vitest", "slug": "vitest" }],
"visibility": "public",
"created_at": "2025-01-20T10:00:00Z"
}curl -X POST https://stackness.dev/api/v1/moves \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"title": "Prompt-Driven TDD Workflow",
"description": "Write the test prompt first...",
"tool_ids": [20]
}'const res = await fetch("https://stackness.dev/api/v1/moves", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Prompt-Driven TDD Workflow",
tool_ids: [20],
}),
});
const data = await res.json();List all moves created by the authenticated user. Supports cursor-based pagination.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 50) |
Response
{
"moves": [
{
"id": 1,
"slug": "prompt-driven-tdd-workflow",
"title": "Prompt-Driven TDD Workflow",
"visibility": "public",
"created_at": "2025-01-20T10:00:00Z"
}
],
"next_cursor": "abc123",
"has_more": false
}curl "https://stackness.dev/api/v1/moves?limit=10" \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/moves?limit=10", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Get full details for a specific move by id or slug. The slug is minted once at creation and never changes, so it is safe to store.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Move id or slug (path param). An all-digits segment is read as an id, anything else as a slug. |
Response
{
"id": 1,
"slug": "prompt-driven-tdd-workflow",
"title": "Prompt-Driven TDD Workflow",
"description": "Write the test prompt first, then implement until green...",
"tools": [{ "name": "Vitest", "slug": "vitest" }],
"visibility": "public",
"created_at": "2025-01-20T10:00:00Z"
}curl https://stackness.dev/api/v1/moves/1 \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/moves/1", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Update a move you created. Sending tool_ids replaces the move's tools; omit it to leave them unchanged.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Move id or slug (path param). An all-digits segment is read as an id, anything else as a slug. |
| title | string | No | Updated title |
| description | string | No | Updated description |
| tool_ids | integer[] | No | Replaces the move's tools; omit to leave them unchanged |
| category_id | integer | No | Category ID (see GET /categories) |
| start_date | string | No | Month you started using the move, RFC3339. Month granularity: the day is ignored and the date is stored as the first of the month |
| end_date | string | No | Month you stopped using the move, RFC3339. Month granularity: the day is ignored |
| cover_image_url | string | No | Cover image URL |
| visibility | string | No | "public", "friends", "private", or "subscribers" |
Response
{
"id": 1,
"slug": "prompt-driven-tdd-workflow",
"title": "Updated title",
"description": "Updated description",
"visibility": "public"
}curl -X PATCH https://stackness.dev/api/v1/moves/1 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"title": "Updated title"}'const res = await fetch("https://stackness.dev/api/v1/moves/1", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ title: "Updated title" }),
});
const data = await res.json();Permanently delete a move you created.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Move id or slug (path param). An all-digits segment is read as an id, anything else as a slug. |
Response
{
"message": "move deleted"
}curl -X DELETE https://stackness.dev/api/v1/moves/1 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/moves/1", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});List all public moves created by a user. Supports cursor-based pagination.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 50) |
Response
{
"moves": [
{
"id": 1,
"slug": "prompt-driven-tdd-workflow",
"title": "Prompt-Driven TDD Workflow",
"created_at": "2025-01-20T10:00:00Z"
}
],
"next_cursor": "abc123",
"has_more": false
}curl "https://stackness.dev/api/v1/users/janedoe/moves?limit=10"const res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/moves?limit=10"
);
const data = await res.json();List public moves across all users with author info plus reaction and comment counts. Supports sorting, text search, category and tool filtering, and cursor-based pagination. Retired moves (those with an end date) and moves by deactivated accounts are excluded. No authentication required.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| sort | string | No | Sort order: popularity (default, by reaction + comment count), recent, or title (query param) |
| q | string | No | Search text matched against move titles and descriptions, max 100 characters (query param) |
| category | string | No | Category slug to filter by (query param) |
| tool | string | No | Tool slug to filter by: only moves that use that tool are returned (query param). Combines with sort, category and limit. An unknown slug returns an empty page, not a 404. |
| cursor | string | No | Pagination cursor from the previous page's next_cursor (query param) |
| limit | integer | No | Items per page (default 20, max 100) (query param) |
Response
{
"moves": [
{
"id": 1,
"slug": "prompt-driven-tdd-workflow",
"title": "Prompt-Driven TDD Workflow",
"description": "Write the test prompt first, then implement until green...",
"category": { "id": 3, "name": "AI Tools", "slug": "ai-tools", "display_order": 3 },
"tools": [{ "id": 20, "name": "Vitest", "slug": "vitest" }],
"is_backdated": false,
"author": {
"id": 1,
"username": "janedoe",
"display_name": "Jane Doe",
"is_pro": false
},
"reaction_count": 12,
"comment_count": 4,
"created_at": "2025-01-20T10:00:00Z",
"updated_at": "2025-01-20T10:00:00Z"
}
],
"next_cursor": "16_1",
"has_more": true
}curl "https://stackness.dev/api/v1/moves/public?sort=popularity&limit=10"const res = await fetch(
"https://stackness.dev/api/v1/moves/public?sort=popularity&limit=10"
);
const data = await res.json();Get full details for a single public-visibility move by id or slug, including author info plus reaction and comment counts. A move that is not public, or whose author is deactivated, returns 404. No authentication required.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Move id or slug (path param). An all-digits segment is read as an id, anything else as a slug. |
Response
{
"id": 1,
"slug": "prompt-driven-tdd-workflow",
"title": "Prompt-Driven TDD Workflow",
"description": "Write the test prompt first, then implement until green...",
"category": { "id": 3, "name": "AI Tools", "slug": "ai-tools", "display_order": 3 },
"tools": [{ "id": 20, "name": "Vitest", "slug": "vitest" }],
"is_backdated": false,
"author": {
"id": 1,
"username": "janedoe",
"display_name": "Jane Doe",
"is_pro": false
},
"reaction_count": 12,
"comment_count": 4,
"created_at": "2025-01-20T10:00:00Z",
"updated_at": "2025-01-20T10:00:00Z"
}curl https://stackness.dev/api/v1/moves/public/prompt-driven-tdd-workflowconst res = await fetch(
"https://stackness.dev/api/v1/moves/public/prompt-driven-tdd-workflow"
);
const data = await res.json();Saved items
5Manage your bookmarks: save tools and moves for later, track their status as you evaluate them, and keep short notes.
Save a tool or a move to your bookmarks with an optional note. New items start with status "saved". Saving the same target twice returns a 409 conflict.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| target_type | string | Yes | "tool" or "move" |
| target_id | integer | Yes | ID of the tool or move to save |
| note | string | No | Optional note, at most 500 characters |
Response
{
"id": 42,
"target_type": "tool",
"target_id": 7,
"status": "saved",
"note": "Try for the side project",
"status_changed_at": "2025-01-20T10:00:00Z",
"created_at": "2025-01-20T10:00:00Z",
"tool": {
"id": 7,
"name": "Neovim",
"slug": "neovim",
"description": "Hyperextensible Vim-based text editor",
"website_url": "https://neovim.io",
"logo_url": "https://cdn.stackness.dev/logos/neovim.png"
}
}curl -X POST https://stackness.dev/api/v1/saved-items \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"target_type": "tool", "target_id": 7, "note": "Try for the side project"}'const res = await fetch("https://stackness.dev/api/v1/saved-items", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ target_type: "tool", target_id: 7, note: "Try for the side project" }),
});
const data = await res.json();List the authenticated user's saved items, optionally filtered by status or target type. Cursor-based pagination; next_cursor is only present when has_more is true. Saved tools include an inline tool object, saved moves an inline move object.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| status | string | No | "saved", "trying", or "done" (query param) |
| type | string | No | "tool" or "move" (query param) |
| cursor | integer | No | Pagination cursor from the previous page's next_cursor (query param) |
| limit | integer | No | Items per page, default 20, max 100 (query param) |
Response
{
"items": [
{
"id": 42,
"target_type": "tool",
"target_id": 7,
"status": "trying",
"note": "Try for the side project",
"status_changed_at": "2025-01-21T09:00:00Z",
"created_at": "2025-01-20T10:00:00Z",
"tool": {
"id": 7,
"name": "Neovim",
"slug": "neovim",
"description": "Hyperextensible Vim-based text editor",
"website_url": "https://neovim.io",
"logo_url": "https://cdn.stackness.dev/logos/neovim.png"
}
},
{
"id": 41,
"target_type": "move",
"target_id": 12,
"status": "saved",
"status_changed_at": "2025-01-19T14:00:00Z",
"created_at": "2025-01-19T14:00:00Z",
"move": {
"id": 12,
"slug": "tdd-with-claude",
"title": "TDD with Claude",
"description": "Let the tests drive the agent"
}
}
],
"next_cursor": 41,
"has_more": true
}curl "https://stackness.dev/api/v1/saved-items?status=trying&limit=20" \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/saved-items?status=trying&limit=20", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Check whether the authenticated user has already saved a specific tool or move. When saved is false the item field is omitted.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| target_type | string | Yes | "tool" or "move" (query param) |
| target_id | integer | Yes | ID of the tool or move to check (query param) |
Response
{
"saved": true,
"item": {
"id": 42,
"target_type": "tool",
"target_id": 7,
"status": "saved",
"note": "Try for the side project",
"status_changed_at": "2025-01-20T10:00:00Z",
"created_at": "2025-01-20T10:00:00Z"
}
}curl "https://stackness.dev/api/v1/saved-items/check?target_type=tool&target_id=7" \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/saved-items/check?target_type=tool&target_id=7", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Update the status or note of a saved item you own. At least one field must be provided. Changing the status also updates status_changed_at.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Saved item ID (path param) |
| status | string | No | "saved", "trying", or "done" |
| note | string | No | Updated note, at most 500 characters; send an empty string to clear it |
Response
{
"id": 42,
"target_type": "tool",
"target_id": 7,
"status": "trying",
"note": "Try for the side project",
"status_changed_at": "2025-01-21T09:00:00Z",
"created_at": "2025-01-20T10:00:00Z"
}curl -X PATCH https://stackness.dev/api/v1/saved-items/42 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"status": "trying"}'const res = await fetch("https://stackness.dev/api/v1/saved-items/42", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ status: "trying" }),
});
const data = await res.json();Remove an item from your bookmarks. Returns 204 with no body on success; deleting an item you do not own returns 403.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Saved item ID (path param) |
Response
Empty response (204 no content)curl -X DELETE https://stackness.dev/api/v1/saved-items/42 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/saved-items/42", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});Reactions
4React to stack tools, moves, stack updates, and blog posts with a fixed emoji set.
List reactions on a target grouped by emoji, sorted alphabetically, with aggregate counts. When called with a valid Authorization header, reacted is true for each emoji the caller has reacted with; for anonymous callers it is always false.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| target_type | string | Yes | "user_tool", "move", "stack_update", or "blog_post" (query param) |
| target_id | integer | Yes | ID of the target entity (query param) |
Response
{
"reactions": [
{
"emoji": "fire",
"count": 3,
"reacted": true
},
{
"emoji": "thumbsup",
"count": 5,
"reacted": false
}
]
}curl "https://stackness.dev/api/v1/reactions?target_type=move&target_id=12"const res = await fetch("https://stackness.dev/api/v1/reactions?target_type=move&target_id=12");
const data = await res.json();List reactions on a target grouped by emoji, sorted alphabetically, with the full count and up to 12 reactors per emoji, oldest reaction first.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| target_type | string | Yes | "user_tool", "move", "stack_update", or "blog_post" (query param) |
| target_id | integer | Yes | ID of the target entity (query param) |
Response
{
"reactions": [
{
"emoji": "fire",
"count": 3,
"users": [
{
"id": 7,
"username": "sarah_chen",
"display_name": "Sarah Chen",
"avatar_url": "https://stackness.dev/avatars/sarah_chen.png"
}
]
}
]
}curl "https://stackness.dev/api/v1/reactions/users?target_type=move&target_id=12"const res = await fetch("https://stackness.dev/api/v1/reactions/users?target_type=move&target_id=12");
const data = await res.json();Toggle your reaction on a target entity. Adding a new emoji returns 201 with the created reaction and created true; sending the same emoji again removes it and returns 200 with created false and no reaction object. Adding a reaction notifies the content owner.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| target_type | string | Yes | "user_tool", "move", "stack_update", or "blog_post" |
| target_id | integer | Yes | ID of the entity to react to |
| emoji | string | Yes | "thumbsup", "fire", "lightbulb", "rocket", "heart", or "eyes" |
Response
{
"reaction": {
"id": 88,
"user_id": 7,
"target_type": "move",
"target_id": 12,
"emoji": "fire"
},
"created": true
}curl -X POST https://stackness.dev/api/v1/reactions \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"target_type": "move", "target_id": 12, "emoji": "fire"}'const res = await fetch("https://stackness.dev/api/v1/reactions", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ target_type: "move", target_id: 12, emoji: "fire" }),
});
const data = await res.json();Delete a reaction by ID. Only the user who created the reaction can delete it. Returns 204 with an empty body.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Reaction ID (path param) |
Response
204 no content (empty response body)curl -X DELETE https://stackness.dev/api/v1/reactions/88 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/reactions/88", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});Feed
3Read your personalized feed, the global updates feed, and user/tool-specific feeds.
Get a personalized feed of activity from users you follow. Includes stack changes, moves, and updates.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 50) |
Response
{
"items": [
{
"id": "feed_456",
"type": "move",
"user": { "username": "janedoe", "display_name": "Jane Doe" },
"move": { "title": "Prompt-Driven TDD Workflow" },
"created_at": "2025-01-20T10:00:00Z"
}
],
"next_cursor": "abc123",
"has_more": true
}curl "https://stackness.dev/api/v1/feed/my?limit=20" \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/feed/my?limit=20", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Get the global updates feed showing recent activity across the platform.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 50) |
Response
{
"items": [
{
"id": "feed_789",
"type": "stack_update",
"user": { "username": "johndoe" },
"title": "Q1 Stack Refresh",
"created_at": "2025-01-20T09:00:00Z"
}
],
"next_cursor": "def456",
"has_more": true
}curl "https://stackness.dev/api/v1/feed/updates?limit=20" \
-H "Authorization: Bearer <token>"const res = await fetch(
"https://stackness.dev/api/v1/feed/updates?limit=20",
{ headers: { Authorization: `Bearer ${token}` } }
);
const data = await res.json();Get the public activity feed for a specific user.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| username | string | Yes | Username (path param) |
| cursor | string | No | Pagination cursor |
| limit | integer | No | Items per page (default 20, max 50) |
Response
{
"items": [
{
"id": "feed_101",
"type": "stack_add",
"tool": { "name": "Bun", "slug": "bun" },
"created_at": "2025-01-20T10:00:00Z"
}
],
"next_cursor": "ghi789",
"has_more": false
}curl "https://stackness.dev/api/v1/users/janedoe/feed?limit=20"const res = await fetch(
"https://stackness.dev/api/v1/users/janedoe/feed?limit=20"
);
const data = await res.json();Notifications
4Read the authenticated user's notifications and mark them as read, one at a time or all at once.
List the authenticated user's notifications, newest first. type is one of follow, reaction, comment, or copy; target_type is one of user_tool, move, stack_update, user, or blog_post. Cursor-based pagination; next_cursor is only present when has_more is true.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cursor | integer | No | Pagination cursor from the previous page's next_cursor (query param) |
| limit | integer | No | Items per page, default 20, max 100 (query param) |
Response
{
"notifications": [
{
"id": 120,
"type": "follow",
"target_type": "user",
"target_id": 1,
"is_read": false,
"created_at": "2025-01-20T10:00:00Z",
"actor": {
"id": 2,
"username": "johndoe",
"display_name": "John Doe",
"avatar_url": "https://cdn.stackness.dev/avatars/2.jpg"
}
},
{
"id": 119,
"type": "reaction",
"target_type": "user_tool",
"target_id": 15,
"is_read": true,
"created_at": "2025-01-19T18:30:00Z",
"actor": {
"id": 3,
"username": "alice",
"display_name": "Alice"
}
}
],
"next_cursor": 119,
"has_more": true
}curl "https://stackness.dev/api/v1/notifications?limit=20" \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/notifications?limit=20", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Get the number of unread notifications for the authenticated user.
Response
{
"count": 3
}curl https://stackness.dev/api/v1/notifications/unread-count \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/notifications/unread-count", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Mark a single notification as read. Returns 204 with no body; a notification that does not exist or is not yours returns 404.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Notification ID (path param) |
Response
Empty response (204 no content)curl -X PATCH https://stackness.dev/api/v1/notifications/120/read \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/notifications/120/read", {
method: "PATCH",
headers: { Authorization: `Bearer ${token}` },
});Mark all of the authenticated user's unread notifications as read and return how many were updated. No request body is required.
Response
{
"marked_count": 5
}curl -X POST https://stackness.dev/api/v1/notifications/read-all \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/notifications/read-all", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Discover & search
5Discover trending tools and moves, top users, rising tools, browse by category, and full-text search.
Get tools trending across the platform based on recent adoption velocity. Always returns at most 10 tools. The window widens from weekly to monthly, quarterly and all-time when the requested one is too quiet; the window actually used is echoed back.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| period | string | No | "weekly" (default) or "monthly" (query param) |
| category | string | No | Category slug (query param). Narrows the ranking to one tool category; an unknown slug returns 404. |
Response
{
"tools": [
{
"id": 20,
"name": "Vite",
"slug": "vite",
"user_count": 450,
"trend_score": 92
}
]
}curl "https://stackness.dev/api/v1/discover/trending-tools?period=weekly&category=ai-tools"const res = await fetch(
"https://stackness.dev/api/v1/discover/trending-tools?period=weekly&category=ai-tools"
);
const data = await res.json();Get the most popular moves on the platform right now. Always returns at most 10 moves, with the same widening window as trending tools.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| category | string | No | Category slug (query param). Narrows the ranking to one tool category; an unknown slug returns 404. |
Response
{
"moves": [
{
"move_id": 5,
"title": "Prompt-Driven TDD Workflow",
"username": "janedoe",
"reaction_count": 42,
"comment_count": 7
}
]
}curl "https://stackness.dev/api/v1/discover/trending-moves?category=ai-tools"const res = await fetch(
"https://stackness.dev/api/v1/discover/trending-moves?category=ai-tools"
);
const data = await res.json();Get the most followed and active users on the platform. Always returns at most 10 users; the endpoint takes no parameters.
Response
{
"users": [
{
"id": 1,
"username": "janedoe",
"display_name": "Jane Doe",
"followers_count": 1200,
"stack_count": 25
}
]
}curl https://stackness.dev/api/v1/discover/top-usersconst res = await fetch(
"https://stackness.dev/api/v1/discover/top-users"
);
const data = await res.json();Get tools with the fastest adoption growth over the past week. Always returns at most 10 tools, with the same widening window as trending tools.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| category | string | No | Category slug (query param). Narrows the ranking to one tool category; an unknown slug returns 404. |
Response
{
"tools": [
{
"id": 30,
"name": "Bun",
"slug": "bun",
"growth_percent": 45.2,
"user_count": 120
}
]
}curl "https://stackness.dev/api/v1/discover/rising-tools?category=ai-tools"const res = await fetch(
"https://stackness.dev/api/v1/discover/rising-tools?category=ai-tools"
);
const data = await res.json();Full-text search across tools, users, and moves. Filter by type for narrower results.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | Search query |
| type | string | No | Filter: "tools", "users", or "moves" |
| limit | integer | No | Max results (default 20, max 100) |
Response
{
"results": [
{
"type": "tool",
"tool": { "name": "PostgreSQL", "slug": "postgresql" }
},
{
"type": "user",
"user": { "username": "janedoe", "display_name": "Jane Doe" }
}
]
}curl "https://stackness.dev/api/v1/search?q=postgres&type=tools"const res = await fetch(
"https://stackness.dev/api/v1/search?q=postgres&type=tools"
);
const data = await res.json();Trends
1Historical popularity series (trend waves) for tools and moves, available to Pro subscribers.
Compute stacked popularity series for tools or moves over a time window. Requires a Pro subscription - non-Pro accounts receive 403. Tool values count active stack entries per bucket; move values are cumulative reaction counts. Bands are picked by peak activity in the window, and each response includes the top rising and falling entries by growth percentage (999.99 marks a series that rose from zero). Results are cached server side for 6 hours.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| type | string | No | "tools" (default) or "moves" (query param) |
| range | string | No | "90d", "6m" (default), "12m", or "all" (query param) |
| granularity | string | No | "week" (default) or "month" (query param) |
| category_id | integer | No | Limit tool activity to one category; applies to the tools type (query param) |
| limit | integer | No | Number of series to return, default 20, max 50 (query param) |
| ids | string | No | Comma-separated tool or move IDs to chart exactly, max 50; overrides limit, and ids with no activity in the window are omitted from the response (query param) |
| lens | string | No | "platform" (default) or "history"; the history lens attaches imported series to each band (query param) |
| history_source | string | No | Required when lens is "history": imported source to attach, e.g. "so_survey", "sede_tags", "npm", "pypi", "gharchive" (query param) |
| history_metric | string | No | Metric filter for the history lens, e.g. "usage_share_pct", "question_count", "downloads" (query param) |
Response
{
"buckets": ["2025-01-06", "2025-01-13", "2025-01-20"],
"series": [
{
"id": 1,
"name": "PostgreSQL",
"slug": "postgresql",
"logo_url": "https://cdn.stackness.dev/logos/postgresql.png",
"values": [118, 124, 131]
}
],
"rising": [
{
"id": 1,
"name": "PostgreSQL",
"growth_pct": 11.0
}
],
"falling": [
{
"id": 7,
"name": "Grunt",
"growth_pct": -12.5
}
]
}curl "https://stackness.dev/api/v1/trends/waves?type=tools&range=6m&granularity=week" \
-H "Authorization: Bearer <token>"const res = await fetch(
"https://stackness.dev/api/v1/trends/waves?type=tools&range=6m&granularity=week",
{ headers: { Authorization: `Bearer ${token}` } },
);
const data = await res.json();Teams
19Create and manage teams, their members and invitations, and team billing through Stripe.
Create a new team. The creator becomes the owner. New teams start on the solo plan with a single seat; upgrade via team billing to invite more members.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Team display name |
| slug | string | Yes | URL slug for the team, must be unique |
| description | string | No | Short team description |
Response
{
"id": 12,
"name": "Acme",
"slug": "acme",
"description": "Platform team at Acme",
"plan": "solo",
"seat_count": 1,
"max_seats": 1,
"created_at": "2025-01-20T10:00:00Z"
}curl -X POST https://stackness.dev/api/v1/teams \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Acme", "slug": "acme", "description": "Platform team at Acme"}'const res = await fetch("https://stackness.dev/api/v1/teams", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Acme", slug: "acme" }),
});
const data = await res.json();List all teams the authenticated user belongs to, with the user's role in each.
Response
{
"teams": [
{
"id": 12,
"name": "Acme",
"slug": "acme",
"role": "owner"
}
]
}curl https://stackness.dev/api/v1/teams \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/teams", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Get a team's public profile by slug, including its plan and seat usage.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
Response
{
"id": 12,
"name": "Acme",
"slug": "acme",
"description": "Platform team at Acme",
"logo_url": "https://cdn.stackness.dev/teams/acme.png",
"website": "https://acme.dev",
"plan": "pro",
"seat_count": 4,
"max_seats": 5,
"created_at": "2025-01-20T10:00:00Z"
}curl https://stackness.dev/api/v1/teams/acmeconst res = await fetch("https://stackness.dev/api/v1/teams/acme");
const data = await res.json();Update a team's name, description, logo, or website (owner or admin only). Omitted fields are left unchanged.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
| name | string | No | Updated team name |
| description | string | No | Updated description |
| logo_url | string | No | Updated logo URL |
| website | string | No | Updated website URL |
Response
{
"id": 12,
"name": "Acme",
"slug": "acme",
"description": "Platform team at Acme",
"logo_url": "https://cdn.stackness.dev/teams/acme.png",
"website": "https://acme.dev",
"plan": "pro",
"seat_count": 4,
"max_seats": 5,
"created_at": "2025-01-20T10:00:00Z"
}curl -X PATCH https://stackness.dev/api/v1/teams/acme \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"description": "Platform team at Acme"}'const res = await fetch("https://stackness.dev/api/v1/teams/acme", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ description: "Platform team at Acme" }),
});
const data = await res.json();Delete a team (owner only). Cancels the team's Stripe subscription if one exists. Returns 204 with no body.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
Response
204 no contentcurl -X DELETE https://stackness.dev/api/v1/teams/acme \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/teams/acme", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});List the members of a team with their roles. Only team members can view the list. Offset-paginated: the page size defaults to 50 and is capped at 100.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
| offset | integer | No | Number of members to skip, default 0 (query param) |
| limit | integer | No | Page size, default 50, max 100 (query param) |
Response
{
"members": [
{
"id": 3,
"user_id": 42,
"username": "sarah_chen",
"display_name": "Sarah Chen",
"avatar_url": "https://cdn.stackness.dev/avatars/sarah.png",
"role": "owner"
}
],
"total_count": 4,
"has_more": false
}curl https://stackness.dev/api/v1/teams/acme/members \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/teams/acme/members", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Change a team member's role (owner only). Setting the role to owner transfers ownership and demotes the current owner to admin. Returns 204 with no body.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
| id | integer | Yes | Team member ID (path param) |
| role | string | Yes | "member", "admin", or "owner" |
Response
204 no contentcurl -X PATCH https://stackness.dev/api/v1/teams/acme/members/3 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'await fetch("https://stackness.dev/api/v1/teams/acme/members/3", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ role: "admin" }),
});Remove a member from a team (owner or admin only). The owner cannot be removed, and only the owner can remove an admin. The team's seat count and Stripe subscription quantity are updated. Returns 204 with no body.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
| id | integer | Yes | Team member ID (path param) |
Response
204 no contentcurl -X DELETE https://stackness.dev/api/v1/teams/acme/members/3 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/teams/acme/members/3", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});List all invitations for a team with their status (owner or admin only). Status is one of pending, accepted, declined, cancelled, or expired.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
Response
{
"invitations": [
{
"id": 7,
"email": "dev@example.com",
"role": "member",
"token": "3f9c2a71d0b64e88a5c1f2e3d4a5b6c7",
"status": "pending",
"expires_at": "2025-01-27T10:00:00Z",
"created_at": "2025-01-20T10:00:00Z"
}
]
}curl https://stackness.dev/api/v1/teams/acme/invitations \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/teams/acme/invitations", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Invite someone to the team by email (owner or admin only). The role defaults to member when omitted. An invitation email is sent and the invitation expires after 7 days. Fails when a pending invitation for the email already exists or the team has no free seats.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
| string | Yes | Email address to invite | |
| role | string | No | "member" (default) or "admin" |
Response
{
"id": 7,
"email": "dev@example.com",
"role": "member",
"token": "3f9c2a71d0b64e88a5c1f2e3d4a5b6c7",
"status": "pending",
"expires_at": "2025-01-27T10:00:00Z",
"created_at": "2025-01-20T10:00:00Z"
}curl -X POST https://stackness.dev/api/v1/teams/acme/invitations \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com", "role": "member"}'const res = await fetch("https://stackness.dev/api/v1/teams/acme/invitations", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "dev@example.com", role: "member" }),
});
const data = await res.json();Cancel a team invitation (owner or admin only). The invitation is marked cancelled and can no longer be accepted. Returns 204 with no body.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
| id | integer | Yes | Invitation ID (path param) |
Response
204 no contentcurl -X DELETE https://stackness.dev/api/v1/teams/acme/invitations/7 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/teams/acme/invitations/7", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});Get the team's billing contact and address (owner only). Unset fields are returned as empty strings.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
Response
{
"billing_email": "billing@acme.dev",
"billing_name": "Acme GmbH",
"billing_address_line1": "Musterstrasse 1",
"billing_address_line2": "",
"billing_city": "Berlin",
"billing_state": "",
"billing_postal_code": "10115",
"billing_country": "DE"
}curl https://stackness.dev/api/v1/teams/acme/billing/info \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/teams/acme/billing/info", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Update the team's billing contact and address (owner only). Omitted fields are left unchanged; the full billing info is returned.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
| billing_email | string | No | Billing contact email |
| billing_name | string | No | Billing name, such as the legal company name |
| billing_address_line1 | string | No | Address line 1 |
| billing_address_line2 | string | No | Address line 2 |
| billing_city | string | No | City |
| billing_state | string | No | State or region |
| billing_postal_code | string | No | Postal code |
| billing_country | string | No | Country code |
Response
{
"billing_email": "billing@acme.dev",
"billing_name": "Acme GmbH",
"billing_address_line1": "Musterstrasse 1",
"billing_address_line2": "",
"billing_city": "Berlin",
"billing_state": "",
"billing_postal_code": "10115",
"billing_country": "DE"
}curl -X PATCH https://stackness.dev/api/v1/teams/acme/billing/info \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"billing_email": "billing@acme.dev", "billing_country": "DE"}'const res = await fetch("https://stackness.dev/api/v1/teams/acme/billing/info", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ billing_email: "billing@acme.dev" }),
});
const data = await res.json();Create a Stripe checkout session to upgrade the team to the pro plan (owner only). The seat quantity is adjustable in checkout. Returns the checkout URL to redirect the owner to.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
Response
{
"url": "https://checkout.stripe.com/c/pay/cs_live_a1B2c3D4"
}curl -X POST https://stackness.dev/api/v1/teams/acme/billing/upgrade \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/teams/acme/billing/upgrade", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Create a Stripe billing portal session for the team (owner only). Requires a completed billing upgrade. Returns the portal URL to redirect the owner to.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | Team slug (path param) |
Response
{
"url": "https://billing.stripe.com/p/session/live_a1B2c3D4"
}curl -X POST https://stackness.dev/api/v1/teams/acme/billing/portal \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/teams/acme/billing/portal", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();List pending, unexpired team invitations addressed to the authenticated user's account email.
Response
{
"invitations": [
{
"id": 7,
"token": "3f9c2a71d0b64e88a5c1f2e3d4a5b6c7",
"email": "dev@example.com",
"role": "member",
"expires_at": "2025-01-27T10:00:00Z",
"created_at": "2025-01-20T10:00:00Z",
"team_name": "Acme",
"team_slug": "acme",
"team_logo_url": "https://cdn.stackness.dev/teams/acme.png",
"inviter_name": "Sarah Chen"
}
]
}curl https://stackness.dev/api/v1/invitations/pending \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/invitations/pending", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Get invitation details by token. Public - the token itself is the access credential, so the invitation page can render before the visitor signs in. A pending invitation past its expiry is reported with status expired.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | Invitation token from the invitation email (path param) |
Response
{
"email": "dev@example.com",
"role": "member",
"status": "pending",
"expires_at": "2025-01-27T10:00:00Z",
"team_name": "Acme",
"team_slug": "acme",
"team_logo_url": "https://cdn.stackness.dev/teams/acme.png",
"inviter_name": "Sarah Chen"
}curl https://stackness.dev/api/v1/invitations/3f9c2a71d0b64e88a5c1f2e3d4a5b6c7const res = await fetch(
"https://stackness.dev/api/v1/invitations/3f9c2a71d0b64e88a5c1f2e3d4a5b6c7",
);
const data = await res.json();Accept a team invitation. Only the user whose account email matches the invitation email can accept. Adds the user to the team with the invited role and updates the team's seat count.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | Invitation token (path param) |
Response
{
"status": "accepted"
}curl -X POST https://stackness.dev/api/v1/invitations/3f9c2a71d0b64e88a5c1f2e3d4a5b6c7/accept \
-H "Authorization: Bearer <token>"const res = await fetch(
"https://stackness.dev/api/v1/invitations/3f9c2a71d0b64e88a5c1f2e3d4a5b6c7/accept",
{
method: "POST",
headers: { Authorization: `Bearer ${token}` },
},
);
const data = await res.json();Decline a team invitation. Only the user whose account email matches the invitation email can decline.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | Invitation token (path param) |
Response
{
"status": "declined"
}curl -X POST https://stackness.dev/api/v1/invitations/3f9c2a71d0b64e88a5c1f2e3d4a5b6c7/decline \
-H "Authorization: Bearer <token>"const res = await fetch(
"https://stackness.dev/api/v1/invitations/3f9c2a71d0b64e88a5c1f2e3d4a5b6c7/decline",
{
method: "POST",
headers: { Authorization: `Bearer ${token}` },
},
);
const data = await res.json();Settings
12Manage notification preferences, privacy and blocked users, connected OAuth accounts, and account lifecycle: deletion, restore, and data export.
Return the authenticated user's notification preferences: the email delivery mode and per-event toggles.
Response
{
"email_notifications": "realtime",
"notify_follows": true,
"notify_reactions": true,
"notify_comments": true,
"notify_copies": true,
"trends_newsletter": true
}curl https://stackness.dev/api/v1/settings/notifications \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/settings/notifications", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Update one or more notification preferences. Only fields present in the body are changed; the full updated settings are returned.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| email_notifications | string | No | "none", "realtime", or "digest" |
| notify_follows | boolean | No | Notify when someone follows you |
| notify_reactions | boolean | No | Notify when someone reacts to your content |
| notify_comments | boolean | No | Notify when someone comments on your content |
| notify_copies | boolean | No | Notify when someone copies a tool from your stack |
| trends_newsletter | boolean | No | Receive the trends newsletter email |
Response
{
"email_notifications": "digest",
"notify_follows": true,
"notify_reactions": false,
"notify_comments": true,
"notify_copies": true,
"trends_newsletter": true
}curl -X PATCH https://stackness.dev/api/v1/settings/notifications \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"email_notifications": "digest", "notify_reactions": false}'const res = await fetch("https://stackness.dev/api/v1/settings/notifications", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email_notifications: "digest", notify_reactions: false }),
});
const data = await res.json();Return the authenticated user's privacy settings: default stack entry visibility, profile visibility, and the activity feed opt-out.
Response
{
"default_visibility": "public",
"profile_visibility": "public",
"hide_activity": false
}curl https://stackness.dev/api/v1/settings/privacy \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/settings/privacy", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Update one or more privacy settings. Only fields present in the body are changed; the full updated settings are returned.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| default_visibility | string | No | Default visibility for new stack entries: "public", "friends", or "private" |
| profile_visibility | string | No | Who can view your profile: "public" or "logged_in" |
| hide_activity | boolean | No | Hide your tool additions, moves, and stack updates from other users' feeds |
Response
{
"default_visibility": "friends",
"profile_visibility": "public",
"hide_activity": true
}curl -X PATCH https://stackness.dev/api/v1/settings/privacy \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"default_visibility": "friends", "hide_activity": true}'const res = await fetch("https://stackness.dev/api/v1/settings/privacy", {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ default_visibility: "friends", hide_activity: true }),
});
const data = await res.json();List all users the authenticated user has blocked, newest first. display_name and avatar_url are omitted when empty.
Response
{
"users": [
{
"id": 42,
"username": "johndoe",
"display_name": "John Doe",
"avatar_url": "https://stackness.dev/avatars/johndoe.png",
"blocked_at": "2025-01-20T10:00:00Z"
}
]
}curl https://stackness.dev/api/v1/settings/privacy/blocked \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/settings/privacy/blocked", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Remove a block on the given user. Returns 204 no content on success, or 404 if the user is not blocked.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| userID | integer | Yes | ID of the blocked user to unblock (path param) |
Response
204 no contentcurl -X DELETE https://stackness.dev/api/v1/settings/privacy/blocked/42 \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/settings/privacy/blocked/42", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});List the OAuth providers linked to the authenticated user's account. The array is empty when no provider is connected.
Response
{
"accounts": [
{
"provider": "github",
"email": "jane@example.com",
"connected_at": "2025-01-20T10:00:00Z"
}
]
}curl https://stackness.dev/api/v1/settings/accounts \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/settings/accounts", {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Start linking a GitHub or Google account to the authenticated user. Returns the OAuth authorization URL to redirect the user to; the link completes on the OAuth callback.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| provider | string | Yes | "github" or "google" (path param) |
Response
{
"url": "https://github.com/login/oauth/authorize?client_id=Iv1.abc123&state=..."
}curl -X POST https://stackness.dev/api/v1/settings/accounts/github \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/settings/accounts/github", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Unlink a GitHub or Google account. Fails with 409 when it is the last remaining sign-in method: a password or another provider must remain. Returns 204 no content on success.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| provider | string | Yes | "github" or "google" (path param) |
Response
204 no contentcurl -X DELETE https://stackness.dev/api/v1/settings/accounts/github \
-H "Authorization: Bearer <token>"await fetch("https://stackness.dev/api/v1/settings/accounts/github", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});Request deletion of the authenticated user's account. Re-authentication is required: send your current password, or your exact username when the account is OAuth-only and has no password. API keys and refresh tokens are revoked immediately, and the account is erased after a 30-day grace period during which it can be restored. Fails with 409 when a deletion is already pending or you are the sole owner of a team.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| password | string | No | Current password; required when the account has a password |
| username_confirm | string | No | Exact username; required for OAuth-only accounts without a password |
Response
{
"deletion_requested_at": "2025-01-20T10:00:00Z",
"grace_period_days": 30
}curl -X DELETE https://stackness.dev/api/v1/settings/account \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"password": "your-password"}'const res = await fetch("https://stackness.dev/api/v1/settings/account", {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ password: "your-password" }),
});
const data = await res.json();Cancel a pending account deletion during the 30-day grace period. Fails with 409 when no deletion is pending.
Response
{
"message": "account restored"
}curl -X POST https://stackness.dev/api/v1/settings/account/restore \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/settings/account/restore", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Request an export of your account data. Returns 202 and runs in the background: a JSON archive is assembled and a download link valid for 7 days arrives by email. Limited to 2 requests per hour.
Response
{
"message": "export started: a download link will arrive by email"
}curl -X POST https://stackness.dev/api/v1/settings/account/export \
-H "Authorization: Bearer <token>"const res = await fetch("https://stackness.dev/api/v1/settings/account/export", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();Uploads
2Upload avatar and content images to Stackness storage and get back a CDN URL.
Upload a profile avatar as a multipart form. Accepts JPEG, PNG, WebP, and GIF up to 2 MB; the type is detected from the file content, not the filename. The image is center-cropped to 256x256 and re-encoded as JPEG before storage. Returns 201 with the CDN URL of the stored file.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| file | file | Yes | Image file: JPEG, PNG, WebP, or GIF, max 2 MB (multipart form field) |
Response
{
"url": "https://cdn.stackness.dev/avatars/1/3f2a9c1e-7b4d-4e2a-9c1e-7b4d4e2a9c1e.jpg"
}curl -X POST https://stackness.dev/api/v1/upload/avatar \
-H "Authorization: Bearer <token>" \
-F "file=@avatar.png"const form = new FormData();
form.append("file", fileInput.files[0]);
const res = await fetch("https://stackness.dev/api/v1/upload/avatar", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form,
});
const data = await res.json();Upload a general-purpose image (screenshots, cover images) as a multipart form. Accepts JPEG, PNG, WebP, and GIF up to 5 MB; the type is detected from the file content and the image is stored in its original format. Returns 201 with the CDN URL of the stored file.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| file | file | Yes | Image file: JPEG, PNG, WebP, or GIF, max 5 MB (multipart form field) |
Response
{
"url": "https://cdn.stackness.dev/images/1/3f2a9c1e-7b4d-4e2a-9c1e-7b4d4e2a9c1e.png"
}curl -X POST https://stackness.dev/api/v1/upload/image \
-H "Authorization: Bearer <token>" \
-F "file=@screenshot.png"const form = new FormData();
form.append("file", fileInput.files[0]);
const res = await fetch("https://stackness.dev/api/v1/upload/image", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form,
});
const data = await res.json();Blog
3Read published Stackness blog posts. Every post carries one category: update, announcement, trend or knowledge (evergreen explainers and guides).
List published blog posts with cursor pagination. Drafts are never returned here.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cursor | integer | No | Cursor from a previous response's next_cursor (query param) |
| limit | integer | No | Page size, default 20, max 100 (query param) |
| category | string | No | One of "update", "announcement", "trend" or "knowledge" (query param). Any other value returns 400 VALIDATION_ERROR. |
| tool | string | No | Tool slug (query param). Returns only published posts whose body links that tool page, newest first. An unknown slug returns an empty page, not a 404. |
| tag | string | No | Tag slug (query param). Returns only published posts carrying that tag, with the same cursor pagination. The value is normalised like stored tags, so AI matches ai. An unused tag returns an empty page. |
Response
{
"posts": [
{
"id": 3,
"title": "Introducing trend waves",
"slug": "introducing-trend-waves",
"body": "Full markdown body of the post...",
"excerpt": "A new Pro chart showing how tool adoption shifts over time.",
"cover_image_url": "https://cdn.stackness.dev/images/1/cover.png",
"status": "published",
"category": "announcement",
"published_at": "2025-06-02T09:00:00Z",
"created_at": "2025-06-01T14:30:00Z",
"updated_at": "2025-06-02T09:00:00Z",
"author": {
"id": 1,
"username": "sarah_chen",
"display_name": "Sarah Chen",
"avatar_url": "https://cdn.stackness.dev/avatars/1/a1b2c3.jpg"
},
"tags": ["trends", "pro"],
"word_count": 640,
"reading_time_minutes": 4,
"mentioned_tools": [
{
"id": 12,
"name": "Git",
"slug": "git",
"logo_url": "https://cdn.stackness.dev/logos/git.png"
}
]
}
],
"next_cursor": 3,
"has_more": true
}curl "https://stackness.dev/api/v1/blog?limit=20&category=trend"const res = await fetch(
"https://stackness.dev/api/v1/blog?limit=20&category=trend"
);
const data = await res.json();Get a single published blog post by its URL slug. Returns 404 if no published post has that slug. The body is sanitised HTML whose h2, h3 and h4 headings carry stable id anchors. tags are lowercase slugs, word_count and reading_time_minutes (200 words a minute, at least 1) are derived from the body, and mentioned_tools lists the catalog tools the body links.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | URL slug of the post (path param) |
Response
{
"id": 3,
"title": "Introducing trend waves",
"slug": "introducing-trend-waves",
"body": "Full markdown body of the post...",
"excerpt": "A new Pro chart showing how tool adoption shifts over time.",
"cover_image_url": "https://cdn.stackness.dev/images/1/cover.png",
"status": "published",
"category": "announcement",
"published_at": "2025-06-02T09:00:00Z",
"created_at": "2025-06-01T14:30:00Z",
"updated_at": "2025-06-02T09:00:00Z",
"author": {
"id": 1,
"username": "sarah_chen",
"display_name": "Sarah Chen",
"avatar_url": "https://cdn.stackness.dev/avatars/1/a1b2c3.jpg"
},
"tags": ["trends", "pro"],
"word_count": 640,
"reading_time_minutes": 4,
"mentioned_tools": [
{
"id": 12,
"name": "Git",
"slug": "git",
"logo_url": "https://cdn.stackness.dev/logos/git.png"
}
]
}curl https://stackness.dev/api/v1/blog/introducing-trend-wavesconst res = await fetch(
"https://stackness.dev/api/v1/blog/introducing-trend-waves",
);
const data = await res.json();Other published posts related to a published post: the ones sharing the most tags first, newest first among equals. When no post shares a tag, the newest posts in the same category are returned instead. The post itself and drafts are never included, and the list can be empty. Returns 404 if no published post has that slug.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| slug | string | Yes | URL slug of the post (path param) |
| limit | integer | No | Maximum posts to return, default 3, max 10 (query param) |
Response
{
"posts": [
{
"id": 5,
"title": "Trend waves, three months in",
"slug": "trend-waves-three-months-in",
"body": "<p>Full HTML body of the post...</p>",
"excerpt": "What the first quarter of trend waves taught us.",
"status": "published",
"category": "trend",
"published_at": "2025-09-01T09:00:00Z",
"created_at": "2025-08-30T10:00:00Z",
"updated_at": "2025-09-01T09:00:00Z",
"author": {
"id": 1,
"username": "sarah_chen",
"display_name": "Sarah Chen"
},
"tags": ["trends"],
"word_count": 980,
"reading_time_minutes": 5,
"mentioned_tools": []
}
]
}curl "https://stackness.dev/api/v1/blog/introducing-trend-waves/related?limit=3"const res = await fetch(
"https://stackness.dev/api/v1/blog/introducing-trend-waves/related?limit=3",
);
const { posts } = await res.json();Billing
5Read the supporter price, check a subscription, and open Stripe checkout or the customer portal.
Returns the public price of the supporter plan. Public so logged out pages can show it. Amounts are in the smallest currency unit, so 489 chf means CHF 4.89. When pricing cannot be resolved the response is still a 200 with available set to false, and the other fields are zero values.
Response
{
"supporter": {
"available": true,
"amount": 489,
"currency": "chf",
"interval": "month",
"interval_count": 1
}
}curl "https://stackness.dev/api/v1/billing/plans"const res = await fetch("https://stackness.dev/api/v1/billing/plans");
const { supporter } = await res.json();Returns the plan, subscription status and renewal date of the authenticated account. billing_enabled is false when the deployment has no Stripe credentials, in which case every account reads as free.
Response
{
"plan": "supporter",
"status": "active",
"current_period_end": "2026-09-16T00:00:00Z",
"cancel_at_period_end": false,
"billing_enabled": true
}curl "https://stackness.dev/api/v1/billing/status" \
-H "Authorization: Bearer YOUR_TOKEN"const res = await fetch("https://stackness.dev/api/v1/billing/status", {
headers: { Authorization: `Bearer ${token}` },
});
const status = await res.json();Creates a Stripe checkout session for the authenticated account and returns the URL to send the browser to. Returns 409 when the account is already subscribed, and 503 when billing is not configured.
Response
{
"url": "https://checkout.stripe.com/c/pay/cs_test_..."
}curl -X POST "https://stackness.dev/api/v1/billing/checkout" \
-H "Authorization: Bearer YOUR_TOKEN"const res = await fetch("https://stackness.dev/api/v1/billing/checkout", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const { url } = await res.json();Creates a Stripe customer portal session so the account can change its payment method or cancel. Returns 400 when the account has never had a subscription, and 503 when billing is not configured.
Response
{
"url": "https://billing.stripe.com/p/session/live_..."
}curl -X POST "https://stackness.dev/api/v1/billing/portal" \
-H "Authorization: Bearer YOUR_TOKEN"const res = await fetch("https://stackness.dev/api/v1/billing/portal", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const { url } = await res.json();Applies a finished checkout session to the authenticated account without waiting for the Stripe webhook, so the success page can show the new plan immediately. Safe to call more than once for the same session.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| session_id | string | Yes | Stripe checkout session id returned on the success redirect |
Response
{
"status": "ok"
}curl -X POST "https://stackness.dev/api/v1/billing/verify-session" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"session_id": "cs_test_a1b2c3"}'const res = await fetch(
"https://stackness.dev/api/v1/billing/verify-session",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ session_id: sessionId }),
},
);
const data = await res.json();Feedback and reports
2Send feedback to the Stackness team and report content or users for moderation.
Submit a contact or feedback message. No authentication is required, but a bearer token may be sent - when it is, the message is linked to that account. Rate limited to 5 submissions per hour per IP. Returns 201 with the stored message ID.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Sender name, max 120 characters |
| string | Yes | Reply-to email address, max 254 characters | |
| subject | string | Yes | Message subject, max 200 characters |
| message | string | Yes | Message body, max 5000 characters |
| cf_turnstile_token | string | No | Cloudflare Turnstile captcha token; verified when captcha is enabled on the deployment |
Response
{
"ok": true,
"id": 42
}curl -X POST https://stackness.dev/api/v1/feedback \
-H "Content-Type: application/json" \
-d '{"name": "Ada Lovelace", "email": "ada@example.com", "subject": "Feature idea", "message": "It would be great to compare two stacks side by side."}'const res = await fetch("https://stackness.dev/api/v1/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Ada Lovelace",
email: "ada@example.com",
subject: "Feature idea",
message: "It would be great to compare two stacks side by side.",
}),
});
const data = await res.json();Report content or a user for moderation review. Each account can report a given target once; reporting the same target again returns 409. Returns 201 with the created report, which starts in the pending status.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| target_type | string | Yes | "user_tool", "move", "comment", or "user" |
| target_id | integer | Yes | ID of the reported content or user |
| reason | string | Yes | "spam", "harassment", "phishing", "inappropriate", or "other" |
| reason_detail | string | No | Free-text detail explaining the report |
Response
{
"id": 15,
"reporter_id": 4,
"target_type": "comment",
"target_id": 128,
"reason": "spam",
"reason_detail": "Repeated link drops for the same site",
"status": "pending",
"created_at": "2025-01-20T10:00:00Z",
"updated_at": "2025-01-20T10:00:00Z"
}curl -X POST https://stackness.dev/api/v1/reports \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"target_type": "comment", "target_id": 128, "reason": "spam"}'const res = await fetch("https://stackness.dev/api/v1/reports", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ target_type: "comment", target_id: 128, reason: "spam" }),
});
const data = await res.json();System
2Identify the build the API is running, and opt out of bulk email.
Turn off one bulk email list for the recipient a signed token identifies. Deliberately unauthenticated: it is the target of the List-Unsubscribe one-click header, which mail providers post to with no session attached. The token is read from the query string when the body carries none, and it names the list it applies to, so a newsletter token cannot switch off the digest. Repeat calls succeed unchanged. Tokens do not expire; they are minted per recipient in each bulk message.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | Signed opt-out token from the email. Accepted in the JSON body or as a query parameter. |
Response
{
"list": "newsletter",
"message": "You have been unsubscribed from the weekly trends newsletter."
}curl -X POST "https://stackness.dev/api/v1/unsubscribe?token=NDI6bmV3c2xldHRlcg.SIGNATURE"const res = await fetch("https://stackness.dev/api/v1/unsubscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: "NDI6bmV3c2xldHRlcg.SIGNATURE" }),
});
const data = await res.json();Returns the commit and build time of the API process serving the request, plus its environment. Touches no database or cache, so it is safe to poll. The values are empty strings when the process was not started by the deploy pipeline.
Response
{
"version": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
"built_at": "2026-08-16T09:30:00Z",
"env": "production"
}curl "https://stackness.dev/api/v1/version"const res = await fetch("https://stackness.dev/api/v1/version");
const data = await res.json();Need programmatic AI access? Check out the MCP server documentation to connect your AI assistant.
Comments
4Comment on stack tools, moves, stack updates, and blog posts, with one level of threaded replies.
List root comments on a target, oldest first, with up to 3 of each root's replies interleaved after it; reply_count carries the full reply total per root. Pass parent_id to page through all replies of one root comment instead; total_count is then that parent's full reply count, otherwise it counts every comment on the target, roots and replies included.
Parameters
Response
Create a comment on a target entity, or reply to a root comment via parent_id (threads are one level deep, so replying to a reply is rejected). Comments containing profanity are rejected. The content owner is notified, and a reply also notifies the parent comment's author. Returns 201 with the created comment.
Parameters
Response
Update the body of one of your comments. Only the comment owner can edit; the same length and profanity checks as creation apply. Returns the updated comment.
Parameters
Response
Delete a comment. Allowed for the comment owner and for the owner of the content the comment is on. Returns 204 with an empty body.
Parameters
Response