/openapi.jsonThis document
Alcance: No se necesita ninguna clave
objectLa API de StackPortfolio permite que tus propios scripts y herramientas lean y actualicen tu perfil y tus proyectos. Es una API JSON que funciona a través de HTTPS y está documentada con un documento OpenAPI 3.1.
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.
URL base: https://stackportfol.io/api/v1
Crea una clave en tu página de claves de API. Asigna solo los permisos que la herramienta necesita y una fecha de caducidad entre 30 y 365 días. La clave se muestra solo una vez, así que guárdala en un lugar seguro; puedes revocarla en cualquier momento.
Envía la clave con cada solicitud, en cualquiera de estos encabezados. Las cookies nunca se utilizan. Una clave que falta, es incorrecta, ha expirado o ha sido revocada siempre devolverá el mismo error 401.
Cada operación requiere un alcance específico, que se muestra en la referencia a continuación. Si una clave no tiene el alcance necesario, se mostrará un error 403 con el código "insufficient_scope". La solicitud GET /me funciona con cualquier clave válida y muestra los alcances asociados a esa clave.
profile:read Lee tu perfilprofile:write Actualiza tu perfilprojects:read Lee tus proyectosprojects:write Crea y actualiza tus proyectosprojects:delete Elimina tus proyectos (acción irreversible)StackPortfolio también es un servidor MCP. Dirige un cliente MCP como Claude a la siguiente dirección: este se registrará, te pedirá que inicies sesión y apruebes los permisos que necesita, y luego realizará las mismas operaciones que la API, con tus permisos. No se necesita ninguna clave de API.
Servidor MCP: 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.
Cada cuenta puede realizar 3 solicitudes por segundo, 30 por minuto, 300 por hora y 1,000 por día, contando todas sus claves. Si se supera cualquiera de estos límites, las solicitudes recibirán un código de error 429 con el código "rate_limited" y un encabezado "Retry-After" que indica los segundos que debes esperar. Cada respuesta incluye los campos "RateLimit-Limit", "RateLimit-Remaining" y "RateLimit-Reset".
Cada error tiene la misma estructura JSON. El código es estable y es seguro crear una rama a partir de él; el mensaje está escrito para las personas y puede cambiar.
unauthorized HTTP 401insufficient_scope HTTP 403not_found HTTP 404validation_failed HTTP 400conflict HTTP 409rate_limited HTTP 429internal HTTP 500Generado a partir de los mismos esquemas que utiliza el servidor para validar las solicitudes. La referencia está en inglés.
/openapi.jsonThis document
Alcance: No se necesita ninguna clave
object/docs/searchSearch developer documentation
Search documentation sections, guides, REST API endpoints, schemas, and MCP tools.
Alcance: profile:read
?query string ?limit integer object/meThe key owner and its scopes
Alcance: cualquier clave válida
Me/profileThe caller's tech profile
Alcance: 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.
Alcance: 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.
Alcance: 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).
Alcance: projects:write
name string obligatoriodescription 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
Alcance: projects:read
{id} string obligatorioProject/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.
Alcance: projects:write
{id} string obligatorioname 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.
Alcance: projects:delete
{id} string obligatorio/projects/{id}/technologiesList a project's technologies
Alcance: projects:read
{id} string obligatorio?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.
Alcance: projects:write
{id} string obligatorioname string obligatorioversion 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.
Alcance: projects:read
{id} string obligatorio?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.
Alcance: projects:write
{id} string obligatoriodata string obligatoriofile 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.
Alcance: projects:write
{id} string obligatoriofilename string obligatoriocontentType "image/png" | "image/jpeg" | "image/webp" obligatoriodescription 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.
Alcance: No se necesita ninguna clave
{token} string obligatorioProjectMedia/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.
Alcance: projects:write
{id} string obligatorio{mediaId} string obligatoriocaption 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.
Alcance: projects:write
{id} string obligatorio{mediaId} string obligatoriodata string obligatoriodescription 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.
Alcance: projects:write
{id} string obligatorio{mediaId} string obligatorio/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.
Alcance: projects:write
{id} string obligatorio{technologyId} string obligatorioversion 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.
Alcance: projects:write
{id} string obligatorio{technologyId} string obligatorio/projects/{id}/contributorsList a project's contributors
Open invitations included. No email addresses.
Alcance: projects:read
{id} string obligatorio?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.
Alcance: projects:write
{id} string obligatorioidentifier string obligatoriorole "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.
Alcance: projects:write
{id} string obligatorio{userId} string obligatoriorole "MAINTAINER" | "CONTRIBUTOR" obligatorioContributor/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.
Alcance: projects:write
{id} string obligatorio{userId} string obligatorio/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.
Alcance: projects:write
{id} string obligatorio{userId} string obligatoriotechnologyIds (string (uuid))[] obligatorioContributor/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.
Alcance: projects:write
{id} string obligatorio{userId} string obligatorioskills string[] obligatorioContributor/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.
Alcance: 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.
Alcance: projects:read
{id} string obligatorio?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.
Alcance: projects:read
{id} string obligatorio{userId} string obligatorioStory/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.
Alcance: projects:write
{id} string obligatorioproblem string obligatorioapproach string obligatoriocorrections 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.
Alcance: projects:write
{id} string obligatorioerror { code: "unauthorized" | "insufficient_scope" | "not_found" | "validation_failed" | "conflict" | "rate_limited" | "internal"; message: string; details: any } obligatoriolimit integer obligatoriooffset integer obligatoriototal integer obligatoriouserId string obligatoriousername string,null obligatorioscopes ("profile:read" | "profile:write" | "projects:read" | "projects:write" | "projects:delete")[] obligatoriovia "api_key" | "oauth" obligatoriouserId string obligatoriotitle string,null obligatorioexperienceLevel string,null obligatoriogithubUsername string,null obligatoriogitlabUsername string,null obligatoriobitbucketUsername string,null obligatorioseekingEmployment boolean obligatorioopenToOpportunities boolean obligatoriopreferredSalaryMin number,null obligatoriopreferredSalaryMax number,null obligatoriosalaryCurrency "USD" | "EUR" | "GBP" | "CAD" | null obligatorioavailableDate string (date-time) | null obligatoriocreatedAt string (date-time) obligatorioupdatedAt string (date-time) obligatorioareas ({ 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 })[] })[] obligatorioindicator { 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 obligatorioid string (uuid) obligatorioname string obligatoriodescription string,null obligatoriorepositoryUrl string,null obligatorioprovider "github" | "gitlab" | "bitbucket" | null obligatorioimage string,null obligatorioimages string[] obligatorioisActive boolean obligatorioisPublic boolean obligatoriostarCount integer obligatoriolastActivityDate string (date-time) obligatoriocreatedAt string (date-time) obligatoriorole "OWNER" | "MAINTAINER" | "CONTRIBUTOR" | null obligatorioaccepted boolean,null obligatoriopinnedPosition integer | null obligatorioscope=mine sets it, and that list is already ordered pins first.id string (uuid) obligatorioname string obligatoriodescription string,null obligatoriorepositoryUrl string,null obligatoriodeployedUrl string,null obligatorioprovider string,null obligatorioimage string,null obligatorioisActive boolean obligatorioisPublic boolean obligatoriodeactivatedAt string (date-time) | null obligatoriovisibility "public" | "unlisted" | "private" obligatorioeffectiveVisibility "public" | "unlisted" | "private" obligatoriostarCount integer obligatoriocommitCount integer obligatorioviewCount integer obligatoriolastActivityDate string (date-time) obligatoriocreatedAt string (date-time) obligatorioviewerRole "OWNER" | "MAINTAINER" | "CONTRIBUTOR" | null obligatorioorganisation { id: string (uuid); name: string; slug: string; isApproved: boolean; isVerified: boolean; logoUrl: string,null } | null obligatorioancestors ({ id: string (uuid); name: string })[] obligatoriotechnologies ({ id: string (uuid); name: string; version: string | null; isApproved: boolean; former: boolean; replacedBy: { id: string (uuid); name: string } | null; categories: ({ slug: string; name: string })[] })[] obligatoriomedia ({ 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 })[] obligatorioid string (uuid) obligatorioname string obligatorioid string (uuid) obligatorioname string obligatorioversion string | null obligatorioisApproved boolean obligatorioformer boolean obligatorioreplacedBy { id: string (uuid); name: string } | null obligatoriocategories ({ slug: string; name: string })[] obligatorioid string (uuid) obligatoriokind "image" | "video_embed" obligatoriourl string obligatorioposterUrl string,null obligatorioembedProvider "youtube" | "vimeo" | "loom" | null obligatorioembedId string,null obligatoriocaption string,null obligatorioaltText string,null obligatorioposition integer obligatorioisCover boolean obligatoriouploadUrl string obligatoriomethod string obligatorioexpiresAt string obligatoriomaxBytes integer obligatorioheaders object obligatoriouserId string obligatorioname string obligatoriousername string,null obligatorioimage string,null obligatoriorole "OWNER" | "MAINTAINER" | "CONTRIBUTOR" obligatorioaccepted boolean obligatorioinvitedAt string (date-time) obligatoriotechnologies ({ id: string (uuid); name: string })[] obligatorioskills ({ id: string (uuid); name: string })[] obligatorioprojectId string (uuid) obligatorioid string obligatoriouserId string obligatorioauthor { name: string,null; username: string,null; image: string,null } obligatorioproblem string obligatorioapproach string obligatoriocorrections string,null obligatoriohindsight string,null obligatoriopractices ({ 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 })[] obligatoriolinks ({ label: string; url: string })[] obligatorioupdatedAt string (date-time) obligatorio