{
  "openapi": "3.1.0",
  "info": {
    "title": "getmy.cv Public API",
    "version": "1.0.0",
    "summary": "Read-only search over the getmy.cv expert index.",
    "description": "getmy.cv is a ranked directory of experts. Position in the index is determined by\nlifetime spend: highest first, ties broken by whoever paid earliest.\n\nThis API is read-only, unauthenticated, and returns published profiles only —\nnever drafts, contact emails, or internal identifiers.\n\n### Ordering\n\nResults are grouped by match quality first, then by lifetime spend *within* each\ngroup. `match: \"strong\"` matched the person's name, headline, or declared skills;\n`\"body\"` matched only their page text; `\"fuzzy\"` is an approximate spelling match\nof a name or headline. `match` is `null` when no `q` was supplied, because a\nfilter-only browse matches nothing — those results are in pure index order.\n\nRanking inside a group reflects what an expert has paid, not relevance. Treat\n`position` as reading order, not as an endorsement.\n\n### Pagination\n\nKeyset (cursor) pagination, never offsets. Pass the `next_cursor` from a response\nback as `cursor`, unmodified, together with the *same* filters. A `next_cursor` of\n`null` means the stream is finished. A cursor this API did not issue is a 400.\n\n### Other agent surfaces\n\nAn MCP server covering the same index is available at `/api/mcp`\n(Streamable HTTP). Per-profile markdown is at `/{username}.md`, and a corpus\nindex at `/llms.txt`.",
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-getmy.cv"
    }
  },
  "servers": [
    {
      "url": "https://getmy.cv/api/v1",
      "description": "This deployment"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "experts",
      "description": "The ranked expert index."
    }
  ],
  "paths": {
    "/experts": {
      "get": {
        "tags": [
          "experts"
        ],
        "operationId": "listExperts",
        "summary": "Search or browse the expert index",
        "description": "Full-text search over names, headlines, skills, location and page content,\nwith a fuzzy fallback that tolerates misspelled names. Omit `q` to browse by\nfacet in pure index order.\n\nMalformed filter values are ignored rather than rejected: a `location`,\n`category` or `skill` that is not a valid slug, a non-numeric `min_score`, and\na `limit` outside 1–100 are dropped, and the request\nstill returns results. The `query` object in the response echoes what was\nactually applied.\n\nThere are exactly two 400s: an undecodable `cursor` (`invalid_cursor`), so a\nclient cannot silently loop over page 1 forever, and a `q` longer than 200\ncharacters (`invalid_request`).",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text query. Quoted phrases, `-exclusions` and `or` are honoured. Whitespace-only is treated as absent.",
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "example": "postgres schema audit"
          },
          {
            "name": "skill",
            "in": "query",
            "required": false,
            "description": "Skill slug the profile must have. Repeat the parameter to require several — they are ANDed. Extras beyond 5 are ignored, as are values that are not valid slugs.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "maxItems": 5,
              "items": {
                "type": "string",
                "maxLength": 64,
                "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
              }
            },
            "example": [
              "postgres",
              "go"
            ]
          },
          {
            "name": "location",
            "in": "query",
            "required": false,
            "description": "Location slug, e.g. `london` or `sao-paulo`.",
            "schema": {
              "type": "string",
              "maxLength": 64,
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Category slug, e.g. `design`. The canonical list is at `/api/mcp` (`list_categories`) or `/llms.txt`.",
            "schema": {
              "type": "string",
              "maxLength": 64,
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
            }
          },
          {
            "name": "min_score",
            "in": "query",
            "required": false,
            "description": "Minimum lifetime spend, in whole US dollars (not cents). Results with a lower score are excluded.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque keyset cursor: the `next_cursor` of a previous response. Must be replayed with identical filters. Anything else — modified, truncated, or longer than 1024 characters — is a 400 rather than a silent restart from the first page.",
            "schema": {
              "type": "string",
              "maxLength": 1024
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page. Out-of-range values fall back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results. An empty `data` array is a valid 200.",
            "headers": {
              "Cache-Control": {
                "description": "Shared-cacheable for 60s.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "`rel=\"service-desc\"` pointing at this document.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Content-Type-Options": {
                "description": "Always `nosniff`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExpertList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_cursor`: the `cursor` was not issued by this API. `invalid_request`: `q` exceeded 200 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The search backend is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ExpertList": {
        "type": "object",
        "required": [
          "object",
          "query",
          "data",
          "returned",
          "total",
          "total_capped",
          "total_cap",
          "next_cursor",
          "has_more",
          "docs"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "type": "string",
            "const": "expert_list"
          },
          "query": {
            "$ref": "#/components/schemas/AppliedQuery"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Expert"
            }
          },
          "returned": {
            "type": "integer",
            "description": "Number of rows in `data`. Never greater than `query.limit`."
          },
          "total": {
            "type": "integer",
            "description": "Total matching published profiles, independent of pagination. Bounded at `total_cap`."
          },
          "total_capped": {
            "type": "boolean",
            "description": "True when the real total is at or above `total_cap`; read `total` as \"at least\" and render e.g. `5,000+`."
          },
          "total_cap": {
            "type": "integer",
            "const": 5000,
            "description": "The bound applied to `total`."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass back as `cursor`. `null` means no further pages."
          },
          "has_more": {
            "type": "boolean",
            "description": "Equivalent to `next_cursor !== null`."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Absolute URL of this OpenAPI document."
          }
        }
      },
      "AppliedQuery": {
        "type": "object",
        "description": "The filter state after normalization — what the server actually applied, with invalid values already dropped.",
        "required": [
          "q",
          "skill",
          "location",
          "category",
          "min_score",
          "limit"
        ],
        "additionalProperties": false,
        "properties": {
          "q": {
            "type": [
              "string",
              "null"
            ]
          },
          "skill": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "min_score": {
            "type": "integer",
            "description": "Whole US dollars. `0` when unfiltered."
          },
          "limit": {
            "type": "integer"
          }
        }
      },
      "Expert": {
        "type": "object",
        "description": "A published profile. Contains public data only — no email address, no internal identifier, and never a draft.",
        "required": [
          "username",
          "display_name",
          "headline",
          "category",
          "category_slug",
          "location",
          "skills",
          "score_cents",
          "score_usd",
          "match",
          "position",
          "profile_url",
          "markdown_url"
        ],
        "additionalProperties": false,
        "properties": {
          "username": {
            "type": "string",
            "description": "Stable public handle; also the profile's URL path."
          },
          "display_name": {
            "type": "string"
          },
          "headline": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "description": "Human-readable category name."
          },
          "category_slug": {
            "type": "string",
            "description": "Pass back as the `category` filter."
          },
          "location": {
            "type": [
              "string",
              "null"
            ],
            "description": "The location as the expert wrote it. This is the display string, not the `location` filter slug."
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "score_cents": {
            "type": "integer",
            "description": "Lifetime spend in cents. This is what determines rank."
          },
          "score_usd": {
            "type": "string",
            "description": "`score_cents` pre-formatted for display, e.g. `$400`."
          },
          "match": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "strong",
              "body",
              "fuzzy",
              null
            ],
            "description": "How the profile matched `q`. `null` when no `q` was supplied."
          },
          "position": {
            "type": "integer",
            "description": "1-based position in this result stream, continuing across cursor pages. NOT the profile's absolute rank in the index."
          },
          "profile_url": {
            "type": "string",
            "format": "uri"
          },
          "markdown_url": {
            "type": "string",
            "format": "uri",
            "description": "Clean markdown rendering of the same profile."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "object",
          "error"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "type": "string",
            "const": "error"
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "additionalProperties": false,
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "invalid_cursor",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}