{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "Marcus Boni — public content API",
    "version": "1.0.0",
    "summary": "Read-only JSON representations of a personal portfolio and technical blog.",
    "description": "Everything published on marcusboni.com.br, as JSON: profile, selected projects,\nclient engagements, technology stack and blog posts.\n\nThe API is **public, read-only and unauthenticated**. There are no API keys, no\nOAuth scopes, no write operations, no webhooks and no published rate limit.\nEvery endpoint below is the production endpoint, which makes the API its own\nsandbox.\n\nErrors are RFC 9457 `application/problem+json` with a stable `code` and a `hint`.\nThe same content is available as Markdown from the HTML URLs via\n`Accept: text/markdown` — see https://marcusboni.com.br/developers.\n\n## Versioning and deprecation\n\nThe major version is in the URL path. `/api/v1` is current; a breaking\nchange would ship as `/api/v2`, never as an edit to `v1`. Additive changes —\na new endpoint, a new optional field — happen in place, so clients should\nignore unknown properties rather than fail on them.\n\nA path being retired is announced on its own responses, before it stops\nanswering: `Deprecation` (RFC 9745) carries the date it was deprecated,\n`Sunset` (RFC 8594) the date it stops responding, and\n`Link: <…>; rel=\"deprecation\"` points at the explanation. There is at least\n**180 days** between the two. Nothing is deprecated today.",
    "contact": {
      "name": "Marcus Boni",
      "email": "mgalvaoboni@gmail.com",
      "url": "https://marcusboni.com.br/developers"
    },
    "license": {
      "name": "Content © Marcus Boni — quote with attribution",
      "url": "https://marcusboni.com.br/developers"
    }
  },
  "externalDocs": {
    "description": "Developer portal, with curl examples for every endpoint",
    "url": "https://marcusboni.com.br/developers"
  },
  "servers": [
    {
      "url": "https://marcusboni.com.br",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "profile",
      "description": "Who the site is about: identity, projects, experience, stack."
    },
    {
      "name": "blog",
      "description": "Published writing, served live from the database."
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiIndex",
        "summary": "List the available operations",
        "description": "Returns the API version, a link to the OpenAPI document, and every operation this API exposes. Use it to discover the surface without parsing the specification.",
        "tags": [
          "profile"
        ],
        "responses": {
          "200": {
            "description": "List the available operations",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          },
          "404": {
            "description": "No such resource. The body names where the real ones are.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET and HEAD are accepted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "406": {
            "description": "The `Accept` header rules out `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profile": {
      "get": {
        "operationId": "getProfile",
        "summary": "Get the profile of Marcus Boni",
        "description": "Identity, current employer, location, public contact channels and résumé links. Use this to answer who the site belongs to and how to reach them.",
        "tags": [
          "profile"
        ],
        "responses": {
          "200": {
            "description": "Get the profile of Marcus Boni",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Profile"
                }
              }
            }
          },
          "404": {
            "description": "No such resource. The body names where the real ones are.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET and HEAD are accepted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "406": {
            "description": "The `Accept` header rules out `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects": {
      "get": {
        "operationId": "listProjects",
        "summary": "List selected personal projects",
        "description": "The projects featured on the site, each with its year, technology stack, bilingual description and public repository URL.",
        "tags": [
          "profile"
        ],
        "responses": {
          "200": {
            "description": "List selected personal projects",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCollection"
                }
              }
            }
          },
          "404": {
            "description": "No such resource. The body names where the real ones are.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET and HEAD are accepted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "406": {
            "description": "The `Accept` header rules out `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/experience": {
      "get": {
        "operationId": "getExperience",
        "summary": "Get professional experience and client engagements",
        "description": "The current role plus every client engagement delivered, each with its sector, scope and stack. Use this to check whether specific industry or technology experience exists.",
        "tags": [
          "profile"
        ],
        "responses": {
          "200": {
            "description": "Get professional experience and client engagements",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Experience"
                }
              }
            }
          },
          "404": {
            "description": "No such resource. The body names where the real ones are.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET and HEAD are accepted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "406": {
            "description": "The `Accept` header rules out `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stack": {
      "get": {
        "operationId": "listTechnologies",
        "summary": "List the technologies worked with",
        "description": "Every technology on the site, grouped by category: frontend, backend, data or ops.",
        "tags": [
          "profile"
        ],
        "responses": {
          "200": {
            "description": "List the technologies worked with",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TechnologyCollection"
                }
              }
            }
          },
          "404": {
            "description": "No such resource. The body names where the real ones are.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET and HEAD are accepted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "406": {
            "description": "The `Accept` header rules out `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts": {
      "get": {
        "operationId": "listPosts",
        "summary": "List published blog posts",
        "description": "Metadata for every published post, newest first. Served live from the database, so a post appears here as soon as it is published.",
        "tags": [
          "blog"
        ],
        "responses": {
          "200": {
            "description": "List published blog posts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostCollection"
                }
              }
            }
          },
          "404": {
            "description": "No such resource. The body names where the real ones are.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET and HEAD are accepted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "406": {
            "description": "The `Accept` header rules out `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The content database could not be reached. The request is worth retrying.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{slug}": {
      "get": {
        "operationId": "getPost",
        "summary": "Get one blog post with its Markdown body",
        "description": "A single post by slug, including the Markdown source it was authored in. Returns 404 when no published post has that slug.",
        "tags": [
          "blog"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The post identifier, as it appears in the blog URL. Lowercase, hyphen-separated.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
            },
            "example": "estudo-de-mercado"
          }
        ],
        "responses": {
          "200": {
            "description": "Get one blog post with its Markdown body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                }
              }
            }
          },
          "404": {
            "description": "No such resource. The body names where the real ones are.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET and HEAD are accepted.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "406": {
            "description": "The `Accept` header rules out `application/json`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The content database could not be reached. The request is worth retrying.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Localized": {
        "type": "object",
        "description": "The same text in both languages the site publishes.",
        "required": [
          "pt",
          "en"
        ],
        "additionalProperties": false,
        "properties": {
          "pt": {
            "type": "string",
            "description": "Brazilian Portuguese."
          },
          "en": {
            "type": "string",
            "description": "English."
          }
        }
      },
      "ApiIndex": {
        "type": "object",
        "description": "Entry point: what this API is and which operations it exposes.",
        "required": [
          "name",
          "description",
          "version",
          "documentation",
          "openapi",
          "endpoints"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "description": "Semantic version of the API contract."
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "method",
                "path",
                "operationId",
                "summary"
              ],
              "properties": {
                "method": {
                  "type": "string",
                  "enum": [
                    "GET"
                  ]
                },
                "path": {
                  "type": "string"
                },
                "operationId": {
                  "type": "string"
                },
                "summary": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Profile": {
        "type": "object",
        "description": "Who the site belongs to, and the public ways to reach them.",
        "required": [
          "name",
          "fullName",
          "role",
          "employer",
          "location",
          "email",
          "links"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The name used publicly."
          },
          "fullName": {
            "type": "string",
            "description": "Full legal name."
          },
          "role": {
            "type": "string",
            "example": "Software Engineer"
          },
          "employer": {
            "type": "object",
            "required": [
              "company",
              "role",
              "since",
              "current"
            ],
            "properties": {
              "company": {
                "type": "string"
              },
              "role": {
                "type": "string"
              },
              "since": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}$",
                "description": "Start month, `YYYY-MM`."
              },
              "current": {
                "type": "boolean",
                "description": "False once the role has ended."
              }
            }
          },
          "location": {
            "type": "object",
            "required": [
              "region",
              "country"
            ],
            "properties": {
              "region": {
                "type": "string",
                "example": "Espírito Santo"
              },
              "country": {
                "type": "string",
                "example": "Brazil"
              }
            }
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "The only contact address published. There is no phone number."
          },
          "bio": {
            "$ref": "#/components/schemas/Localized"
          },
          "repositoryCount": {
            "type": "integer",
            "minimum": 0,
            "description": "Public repositories on GitHub at the last site update."
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "BCP 47 tags for the languages the site is published in."
          },
          "links": {
            "type": "array",
            "description": "Public profiles. Do not infer channels that are not listed.",
            "items": {
              "type": "object",
              "required": [
                "label",
                "handle",
                "url"
              ],
              "properties": {
                "label": {
                  "type": "string"
                },
                "handle": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          },
          "resumes": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "language",
                "url"
              ],
              "properties": {
                "language": {
                  "type": "string",
                  "enum": [
                    "pt-BR",
                    "en"
                  ]
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          }
        }
      },
      "Project": {
        "type": "object",
        "description": "A personal project featured on the site.",
        "required": [
          "id",
          "title",
          "year",
          "description",
          "stack",
          "repositoryUrl"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Editorial index shown on the site.",
            "example": "01"
          },
          "title": {
            "type": "string"
          },
          "year": {
            "type": "string",
            "pattern": "^\\d{4}$"
          },
          "description": {
            "$ref": "#/components/schemas/Localized"
          },
          "stack": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "repositoryUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "ProjectCollection": {
        "type": "object",
        "description": "Selected personal projects.",
        "required": [
          "count",
          "items"
        ],
        "additionalProperties": false,
        "properties": {
          "count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of entries in `items`. The API is not paginated."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Project"
            }
          }
        }
      },
      "Engagement": {
        "type": "object",
        "description": "One client project delivered in the current role.",
        "required": [
          "id",
          "client",
          "project",
          "sector",
          "scope",
          "stack"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "client": {
            "type": "string"
          },
          "project": {
            "$ref": "#/components/schemas/Localized"
          },
          "sector": {
            "$ref": "#/components/schemas/Localized"
          },
          "scope": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Localized"
              }
            ],
            "description": "What was actually built and delivered."
          },
          "stack": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Experience": {
        "type": "object",
        "description": "The current professional role and everything delivered in it.",
        "required": [
          "role",
          "company",
          "startedAt",
          "endedAt",
          "current",
          "engagements"
        ],
        "properties": {
          "role": {
            "type": "string"
          },
          "company": {
            "type": "string"
          },
          "startedAt": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}$"
          },
          "endedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "`null` while the role is ongoing."
          },
          "current": {
            "type": "boolean"
          },
          "engagements": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Engagement"
            }
          }
        }
      },
      "Technology": {
        "type": "object",
        "required": [
          "name",
          "category"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "frontend",
              "backend",
              "data",
              "ops"
            ],
            "description": "Where the technology sits in the stack."
          }
        }
      },
      "TechnologyCollection": {
        "type": "object",
        "description": "Technologies worked with.",
        "required": [
          "count",
          "items"
        ],
        "additionalProperties": false,
        "properties": {
          "count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of entries in `items`. The API is not paginated."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Technology"
            }
          }
        }
      },
      "PostSummary": {
        "type": "object",
        "description": "Metadata for one published post, without its body.",
        "required": [
          "slug",
          "url",
          "title",
          "language",
          "publishedAt",
          "tags"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "Identifier and last URL segment."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": "string"
          },
          "excerpt": {
            "type": "string",
            "description": "Plain-text summary, at most 200 characters."
          },
          "language": {
            "type": "string",
            "enum": [
              "pt-BR",
              "en"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "readingMinutes": {
            "type": "integer",
            "minimum": 1
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601. `null` only for a post that has never been published."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "translationOf": {
            "type": [
              "string",
              "null"
            ],
            "description": "Slug of the same post in the other language, when one exists."
          },
          "coverUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "PostCollection": {
        "type": "object",
        "description": "Published posts, newest first.",
        "required": [
          "count",
          "items"
        ],
        "additionalProperties": false,
        "properties": {
          "count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of entries in `items`. The API is not paginated."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PostSummary"
            }
          }
        }
      },
      "Post": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PostSummary"
          },
          {
            "type": "object",
            "required": [
              "body"
            ],
            "properties": {
              "body": {
                "type": "string",
                "description": "The Markdown source the post was authored in. Image references are resolved to absolute URLs."
              }
            }
          }
        ],
        "description": "A post with its Markdown body."
      },
      "Problem": {
        "type": "object",
        "description": "An error, as RFC 9457 `application/problem+json`, extended with a stable `code` and a `hint` naming the next thing to try.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "hint"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Stable URI identifying the error kind."
          },
          "title": {
            "type": "string",
            "description": "Short, human-readable summary."
          },
          "status": {
            "type": "integer",
            "description": "Repeats the HTTP status code."
          },
          "detail": {
            "type": "string",
            "description": "What went wrong with this request."
          },
          "instance": {
            "type": "string",
            "description": "The path that produced the error."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code, stable across releases.",
            "enum": [
              "not_found",
              "method_not_allowed",
              "not_acceptable",
              "upstream_unavailable"
            ]
          },
          "hint": {
            "type": "string",
            "description": "A concrete resolution step."
          }
        }
      }
    }
  }
}
