/openapi.jsonThis document
Scope: no key needed
objectThe Stackportfolio API lets your own scripts and tools read and update your profile and projects. It is a JSON API over HTTPS, described by an OpenAPI 3.1 document.
Stackportfolio provides a unified developer platform accessible via standard REST endpoints and the Model Context Protocol (MCP). Authenticate requests using Personal API keys with Bearer authorization.
Base URL: https://stackportfol.io/api/v1
Create a key on your API keys page. Give it only the scopes the tool needs and an expiry between 30 and 365 days. The key is shown once, so store it somewhere safe; you can revoke it at any time.
Send the key with every request, in either of these headers. Cookies are never used. A key that is missing, wrong, expired or revoked always gets the same 401.
Each operation needs one scope, shown in the reference below. A key without it gets a 403 with the code insufficient_scope. GET /me works with any valid key and lists the key's scopes.
profile:read Read your profileprofile:write Update your profileprojects:read Read your projectsprojects:write Create and update your projectsprojects:delete Delete your projects (irreversible)Stackportfolio is also an MCP server. Point an MCP client such as Claude at the address below: it registers itself, asks you to sign in and approve the scopes it wants, and then calls the same operations as the API, with your permissions. No API key is needed.
MCP server: https://stackportfol.io/mcp
Stackportfolio exposes client-side Web MCP discovery via navigator.modelContext. Browser-based AI agents can discover tools natively in the user's active session
without passing manual credentials.
List, create, and manage projects. Pass scope=mine to retrieve projects you own or maintain, or scope=visible to browse public projects.
Publish structured "How I worked on this" stories detailing problems, engineering approaches, corrections, and practices.
Each account may make 3 requests per second, 30 per minute, 300 per hour and 1,000 per day, counted across all of its keys. Past any of these, requests get a 429 with the code rate_limited and a Retry-After header giving the seconds to wait. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
Every error has the same JSON shape. The code is stable and safe to branch on; the message is written for people and may change.
unauthorized HTTP 401insufficient_scope HTTP 403not_found HTTP 404validation_failed HTTP 400conflict HTTP 409rate_limited HTTP 429internal HTTP 500Generated from the same schemas the server validates requests with. The reference is in English.
/openapi.jsonThis document
Scope: no key needed
object/docs/searchSearch developer documentation
Search documentation sections, guides, REST API endpoints, schemas, and MCP tools.
Scope: profile:read
?query string ?limit integer object/meThe key owner and its scopes
Scope: any valid key
Me/profileThe caller's tech profile
Scope: profile:read
Profile/profileUpdate the caller's tech profile
A partial update merged onto the stored profile; null clears a field. A GET body can be sent back as is.
Scope: profile:write
title string experienceLevel "junior" | "mid" | "senior" | "lead" githubUsername string gitlabUsername string bitbucketUsername string seekingEmployment boolean openToOpportunities boolean preferredSalaryMin integer preferredSalaryMax integer salaryCurrency "USD" | "EUR" | "GBP" | "CAD" availableDate string yyyy-mm-dd, or a full ISO date-time.Profile/projectsList projects
mine is every project the caller is on, private ones and open invitations included; visible is every project they may read.
Scope: projects:read
?scope "mine" | "visible" ?limit integer ?offset integer { data: ProjectSummary[]; pagination: Pagination }/projectsCreate a project
The caller becomes its OWNER. Visibility defaults to private. Listed technologies are credited to the caller. Past the active-project limit the project is still created, with deactivatedAt set. Project creation has its own hourly and daily limit (429).
Scope: projects:write
name string requireddescription string repositoryUrl string deployedUrl string organisation string technologies (string | { name: string; version: string | null })[] | string contributors (string | { name: string; version: string | null })[] | string visibility "public" | "unlisted" | "private" saveAsDraft boolean parentId string (uuid) | null Project/projects/{id}Get a project
Scope: projects:read
{id} string requiredProject/projects/{id}Update a project
A partial update; null clears a field. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string requiredname string description string repositoryUrl string deployedUrl string organisation string | { name: string } technologies (string | { name: string; version: string | null })[] | string formerRows string[] former string[] replacedBy string[] visibility "public" | "unlisted" | "private" isArchived boolean isActive boolean isArchived; either may be sent.deactivated boolean false activates the project, which is a 409 when the owner is already at their active-project limit.Project/projects/{id}Delete a project
Irreversible. Its technologies, contributors, media and evidence go with it and every credited contributor is rescored; its parts are kept and become root projects. Needs the separate projects:delete scope, which projects:write does not imply. Only the accepted OWNER may delete; anyone else gets the same 404 as for a missing project.
Scope: projects:delete
{id} string required/projects/{id}/technologiesList a project's technologies
Scope: projects:read
{id} string required?limit integer ?offset integer { data: ProjectTechnology[]; pagination: Pagination }/projects/{id}/technologiesAdd a technology
Credits every accepted contributor with it. A project lists at most 50 technologies; one more is a 409. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string requiredname string requiredversion string | null former boolean replacedBy string (uuid) | null ProjectTechnology/projects/{id}/mediaList a project's media
Lists all media items for a project in gallery order.
Scope: projects:read
{id} string required?limit integer ?offset integer { data: ProjectMedia[]; pagination: Pagination }/projects/{id}/mediaAdd an image to a project
Optimises the image server-side and appends it to the project's gallery; the first image becomes the cover. Takes a JSON body with base64 data, or multipart/form-data with a file part and an optional description. The decoded image may be at most 5 MB. A project holds a limited number of images set by its owner's plan; one more is a 409. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string requireddata string requiredfile field.; min length 1description string ProjectMedia/projects/{id}/media/uploadsCreate a single-use upload URL for a project image
Generates a short-lived (10 min), single-use HMAC-signed PUT URL to upload a project image directly without base64 encoding. Accepts PNG, JPEG, or WebP up to 5 MB. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string requiredfilename string requiredcontentType "image/png" | "image/jpeg" | "image/webp" requireddescription string altText string ProjectMediaUpload/uploads/{token}Upload an image to a signed URL
Authorised by the signed URL alone, no key. The body is the raw PNG, JPEG or WebP bytes (not multipart) with the Content-Type from createProjectMediaUpload, at most 5 MB. The URL works once and expires after 10 minutes; a used URL is a 409, an expired one a 410, an oversized body a 413. Responds with the stored image.
Scope: no key needed
{token} string requiredProjectMedia/projects/{id}/media/{mediaId}Update a project's media item
Updates metadata (caption, alt text), position/sort order, or cover status for a media item. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string required{mediaId} string requiredcaption string | null altText string | null position integer sortOrder integer makeCover boolean ProjectMedia/projects/{id}/media/{mediaId}Replace a project's media image
Replaces the image file with a new upload (base64 data or multipart file). Optimises to WebP with responsive variants and deletes the old stored objects. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string required{mediaId} string requireddata string requireddescription string ProjectMedia/projects/{id}/media/{mediaId}Remove a project's media item
Deletes the media item and its stored files (including variants), repairing cover and position ordering. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string required{mediaId} string required/projects/{id}/technologies/{technologyId}Update a technology on a project
Marks a technology former (moved off), optionally naming the technology that replaced it. Send only the keys to change. A former technology still counts for proficiency, so nobody is rescored. A technology the project does not list and a project the caller may not edit are the same 404. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string required{technologyId} string requiredversion string | null | string former boolean replacedBy string (uuid) | null ProjectTechnology/projects/{id}/technologies/{technologyId}Remove a technology
Deletes the evidence it gave on this project and rescores its contributors. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string required{technologyId} string required/projects/{id}/contributorsList a project's contributors
Open invitations included. No email addresses.
Scope: projects:read
{id} string required?limit integer ?offset integer { data: Contributor[]; pagination: Pagination }/projects/{id}/contributorsInvite a contributor
Invites an existing user by username or email; they are notified and must accept. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string requiredidentifier string requiredrole "MAINTAINER" | "CONTRIBUTOR" Contributor/projects/{id}/contributors/{userId}Change a contributor's role
The owner's role cannot be changed (409). Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string required{userId} string requiredrole "MAINTAINER" | "CONTRIBUTOR" requiredContributor/projects/{id}/contributors/{userId}Remove a contributor
Also cancels an open invitation, deletes their evidence on this project and rescores them. Only an accepted OWNER or MAINTAINER may write; anyone else gets the same 404 as for a missing project.
Scope: projects:write
{id} string required{userId} string required/projects/{id}/contributors/{userId}/technologiesSet which technologies a contributor used
Replaces one accepted contributor's technology credits on this project and rescores them; proficiency stays computed. The caller may set their own, and an accepted OWNER or MAINTAINER anyone's but the owner's. A pending invitee, a target the caller may not change, and a project the caller is not on are the same 404.
Scope: projects:write
{id} string required{userId} string requiredtechnologyIds (string (uuid))[] requiredContributor/projects/{id}/contributors/{userId}/skillsSet the skills a contributor used
Replaces the caller's own skills on this project and recomputes their skill levels. userId must be the caller: nobody records skills for someone else, and any other id is a 404.
Scope: projects:write
{id} string required{userId} string requiredskills string[] requiredContributor/storiesList contribution stories
Every "How I worked on this" account on the projects the caller may see, newest project activity first. mine keeps only the callerβs own stories.
Scope: projects:read
?scope "all" | "mine" ?limit integer ?offset integer { data: Story[]; pagination: Pagination }/projects/{id}/storiesList a project's contribution stories
Every "How I worked on this" account written on the project, as its page renders them. mine keeps only the callerβs own.
Scope: projects:read
{id} string required?scope "all" | "mine" ?limit integer ?offset integer { data: Story[]; pagination: Pagination }/projects/{id}/stories/{userId}Get one contributor's story
One contributor's story on a project the caller may see. Any author's story is readable, as the project page renders it; pass the caller's own id from /me to read theirs.
Scope: projects:read
{id} string required{userId} string requiredStory/projects/{id}/storyWrite the caller's story
Writes the callerβs own "How I worked on this" account, replacing any previous one. Every accepted contributor may write theirs, and nobody β owner included β may write anybody elseβs: a project the caller is not on and a missing one are the same 404. Declaring a practice makes corrections mandatory, which is the anti-AI-washing rule the web editor enforces.
Scope: projects:write
{id} string requiredproblem string requiredapproach string requiredcorrections string hindsight string practices ({ tool: string; role: "EXPLORED_OPTIONS" | "WROTE_FIRST_DRAFT" | "REVIEWED_MY_WORK" | "DID_REPETITIVE_PART" | "BUILT_END_TO_END" | "SOMETHING_ELSE"; askedFor: string })[] links ({ label: string; url: string })[] Story/projects/{id}/storyDelete the caller's story
Deletes the callerβs own story on the project. The contribution itself is untouched: only the write-up goes. Only ever the callerβs own story, so projects:write is enough.
Scope: projects:write
{id} string requirederror { code: "unauthorized" | "insufficient_scope" | "not_found" | "validation_failed" | "conflict" | "rate_limited" | "internal"; message: string; details: any } requiredlimit integer requiredoffset integer requiredtotal integer requireduserId string requiredusername string,null requiredscopes ("profile:read" | "profile:write" | "projects:read" | "projects:write" | "projects:delete")[] requiredvia "api_key" | "oauth" requireduserId string requiredtitle string,null requiredexperienceLevel string,null requiredgithubUsername string,null requiredgitlabUsername string,null requiredbitbucketUsername string,null requiredseekingEmployment boolean requiredopenToOpportunities boolean requiredpreferredSalaryMin number,null requiredpreferredSalaryMax number,null requiredsalaryCurrency "USD" | "EUR" | "GBP" | "CAD" | null requiredavailableDate string (date-time) | null requiredcreatedAt string (date-time) requiredupdatedAt string (date-time) requiredareas ({ slug: string; name: string; group: "languages" | "frontend" | "backend" | "data" | "infrastructure" | "developer-tooling" | "quality" | "mobile" | "ai" | "other"; projectCount: number; level: number; technologies: ({ id: string; name: string })[] })[] requiredindicator { label: "specialist" | "generalist" | null; breadth: number; depth: number; topArea: { slug: "languages" | "frontend" | "backend" | "data" | "infrastructure" | "developer-tooling" | "quality" | "mobile" | "ai" | "other"; name: string }; projectCount: number } | null requiredid string (uuid) requiredname string requireddescription string,null requiredrepositoryUrl string,null requiredprovider "github" | "gitlab" | "bitbucket" | null requiredimage string,null requiredimages string[] requiredisActive boolean requiredisPublic boolean requiredstarCount integer requiredlastActivityDate string (date-time) requiredcreatedAt string (date-time) requiredrole "OWNER" | "MAINTAINER" | "CONTRIBUTOR" | null requiredaccepted boolean,null requiredpinnedPosition integer | null requiredscope=mine sets it, and that list is already ordered pins first.id string (uuid) requiredname string requireddescription string,null requiredrepositoryUrl string,null requireddeployedUrl string,null requiredprovider string,null requiredimage string,null requiredisActive boolean requiredisPublic boolean requireddeactivatedAt string (date-time) | null requiredvisibility "public" | "unlisted" | "private" requiredeffectiveVisibility "public" | "unlisted" | "private" requiredstarCount integer requiredcommitCount integer requiredviewCount integer requiredlastActivityDate string (date-time) requiredcreatedAt string (date-time) requiredviewerRole "OWNER" | "MAINTAINER" | "CONTRIBUTOR" | null requiredorganisation { id: string (uuid); name: string; slug: string; isApproved: boolean; isVerified: boolean; logoUrl: string,null } | null requiredancestors ({ id: string (uuid); name: string })[] requiredtechnologies ({ id: string (uuid); name: string; version: string | null; isApproved: boolean; former: boolean; replacedBy: { id: string (uuid); name: string } | null; categories: ({ slug: string; name: string })[] })[] requiredmedia ({ id: string (uuid); kind: "image" | "video_embed"; url: string; posterUrl: string,null; embedProvider: "youtube" | "vimeo" | "loom" | null; embedId: string,null; caption: string,null; altText: string,null; position: integer; isCover: boolean })[] requiredid string (uuid) requiredname string requiredid string (uuid) requiredname string requiredversion string | null requiredisApproved boolean requiredformer boolean requiredreplacedBy { id: string (uuid); name: string } | null requiredcategories ({ slug: string; name: string })[] requiredid string (uuid) requiredkind "image" | "video_embed" requiredurl string requiredposterUrl string,null requiredembedProvider "youtube" | "vimeo" | "loom" | null requiredembedId string,null requiredcaption string,null requiredaltText string,null requiredposition integer requiredisCover boolean requireduploadUrl string requiredmethod string requiredexpiresAt string requiredmaxBytes integer requiredheaders object requireduserId string requiredname string requiredusername string,null requiredimage string,null requiredrole "OWNER" | "MAINTAINER" | "CONTRIBUTOR" requiredaccepted boolean requiredinvitedAt string (date-time) requiredtechnologies ({ id: string (uuid); name: string })[] requiredskills ({ id: string (uuid); name: string })[] requiredprojectId string (uuid) requiredid string requireduserId string requiredauthor { name: string,null; username: string,null; image: string,null } requiredproblem string requiredapproach string requiredcorrections string,null requiredhindsight string,null requiredpractices ({ technologyId: string (uuid); tool: string; toolApproved: boolean; role: "EXPLORED_OPTIONS" | "WROTE_FIRST_DRAFT" | "REVIEWED_MY_WORK" | "DID_REPETITIVE_PART" | "BUILT_END_TO_END" | "SOMETHING_ELSE"; askedFor: string })[] requiredlinks ({ label: string; url: string })[] requiredupdatedAt string (date-time) required