{
  "openapi": "3.1.0",
  "info": {
    "title": "EroLove public catalog",
    "version": "1.1.0",
    "description": "Read-only 18+ catalog of EroLove companions and roleplay scenarios for directories, partners and agents: texts in nine languages, photos, videos, covers, links and prices. Chat is not a public API: people open chat_url or scene_url in a browser. Partners add ?aff=<slug> (and optionally &s1=<sub id>) to any request, and every page link in the answer carries it. The same data is served over MCP at https://erolove.ai/mcp (streamable HTTP; server card at /.well-known/mcp/server-card.json). Within 1.x fields are only added, never removed or renamed.",
    "contact": { "email": "hi@erolove.ai" }
  },
  "servers": [{ "url": "https://erolove.ai" }],
  "paths": {
    "/api/v1/companions": {
      "get": {
        "summary": "Every companion, or a search",
        "operationId": "listCompanions",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string", "maxLength": 100 }, "description": "Optional search over name, gender, city, occupation, tags and scenario titles" },
          { "$ref": "#/components/parameters/lang" },
          { "$ref": "#/components/parameters/aff" },
          { "$ref": "#/components/parameters/s1" }
        ],
        "responses": {
          "200": { "description": "The catalog", "headers": { "ETag": { "$ref": "#/components/headers/ETag" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompanionFeed" } } } },
          "304": { "description": "Unchanged since the ETag sent in If-None-Match" },
          "429": { "description": "More than 30 requests a minute from one IP" }
        }
      }
    },
    "/api/v1/companions/{id}": {
      "get": {
        "summary": "One companion",
        "operationId": "getCompanion",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Companion id from the feed, e.g. rae" },
          { "$ref": "#/components/parameters/lang" },
          { "$ref": "#/components/parameters/aff" },
          { "$ref": "#/components/parameters/s1" }
        ],
        "responses": {
          "200": { "description": "The listing", "headers": { "ETag": { "$ref": "#/components/headers/ETag" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Companion" } } } },
          "304": { "description": "Unchanged" },
          "404": { "description": "Unknown id" }
        }
      }
    },
    "/api/v1/scenarios": {
      "get": {
        "summary": "Every roleplay scenario",
        "operationId": "listScenarios",
        "parameters": [
          { "name": "companion", "in": "query", "schema": { "type": "string" }, "description": "Only the scenarios of this companion id" },
          { "$ref": "#/components/parameters/lang" },
          { "$ref": "#/components/parameters/aff" },
          { "$ref": "#/components/parameters/s1" }
        ],
        "responses": {
          "200": { "description": "The scenarios", "headers": { "ETag": { "$ref": "#/components/headers/ETag" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScenarioFeed" } } } },
          "304": { "description": "Unchanged" },
          "429": { "description": "More than 30 requests a minute from one IP" }
        }
      }
    },
    "/api/v1/scenarios/{id}": {
      "get": {
        "summary": "One scenario",
        "operationId": "getScenario",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Scenario id from the feed, e.g. the-rain" },
          { "$ref": "#/components/parameters/lang" },
          { "$ref": "#/components/parameters/aff" },
          { "$ref": "#/components/parameters/s1" }
        ],
        "responses": {
          "200": { "description": "The scenario", "headers": { "ETag": { "$ref": "#/components/headers/ETag" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Scenario" } } } },
          "304": { "description": "Unchanged" },
          "404": { "description": "Unknown id" }
        }
      }
    },
    "/api/characters": {
      "get": {
        "summary": "Thin list used by the web app itself; directories should read /api/v1/companions",
        "operationId": "listCharactersApp",
        "responses": { "200": { "description": "id, name, tagline, openingMessage, media" } }
      }
    }
  },
  "components": {
    "parameters": {
      "lang": { "name": "lang", "in": "query", "schema": { "type": "string", "enum": ["en", "ru", "uk", "pt", "es", "de", "fr", "it", "pl"], "default": "en" }, "description": "Language of names, texts and page links; a field with no translation falls back to English. The i18n object always carries every language." },
      "aff": { "name": "aff", "in": "query", "schema": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$" }, "description": "Partner slug: every page link in the answer gets ?aff=<slug>, the first-click attribution of the affiliate program (90 days). Media URLs are never tagged." },
      "s1": { "name": "s1", "in": "query", "schema": { "type": "string", "pattern": "^[\\w.-]{1,60}$" }, "description": "Optional sub id added to every tagged link, shown per placement in the partner dashboard" }
    },
    "headers": {
      "ETag": { "schema": { "type": "string" }, "description": "Send it back as If-None-Match to get 304 while nothing changed" }
    },
    "schemas": {
      "Picture": {
        "type": "object",
        "description": "One picture in three files: jpg and webp up to 1600 px on the long side, and an 800 px webp",
        "properties": {
          "jpg": { "type": ["string", "null"], "format": "uri" },
          "webp": { "type": ["string", "null"], "format": "uri" },
          "w800": { "type": ["string", "null"], "format": "uri" },
          "width": { "type": ["integer", "null"] },
          "height": { "type": ["integer", "null"] }
        }
      },
      "Photo": {
        "allOf": [{ "$ref": "#/components/schemas/Picture" }, { "type": "object", "properties": { "caption": { "type": ["string", "null"] } } }]
      },
      "Video": {
        "type": "object",
        "properties": {
          "role": { "type": "string", "enum": ["preview", "post"], "description": "preview: short silent loop for cards and hover; post: a video post of the companion's feed" },
          "mp4": { "type": "string", "format": "uri" },
          "width": { "type": ["integer", "null"] },
          "height": { "type": ["integer", "null"] },
          "poster": { "oneOf": [{ "$ref": "#/components/schemas/Picture" }, { "type": "null" }] },
          "caption": { "type": ["string", "null"] }
        }
      },
      "Pricing": {
        "type": "object",
        "properties": {
          "currency": { "type": "string" },
          "free": { "type": "object", "properties": { "messages_per_day": { "type": "integer" }, "card_required": { "type": "boolean" } } },
          "premium": { "type": ["object", "null"], "properties": { "usd_month": { "type": "number" }, "usd_year": { "type": "number" }, "messages_per_day": { "type": "integer" } } },
          "devoted": { "type": ["object", "null"], "description": "null while not on sale", "properties": { "usd_month": { "type": "number" }, "usd_year": { "type": "number" }, "messages_per_day": { "type": "integer" } } }
        }
      },
      "Meta": {
        "type": "object",
        "properties": {
          "platform": { "type": "string" },
          "url": { "type": "string", "format": "uri", "description": "Home page, tagged when aff is given" },
          "nsfw": { "type": "boolean" },
          "age_rating": { "type": "string" },
          "count": { "type": "integer", "description": "Companions on the site" },
          "scenario_count": { "type": "integer" },
          "contact": { "type": "string" },
          "version": { "type": "string", "description": "Listing shape version" },
          "language": { "type": "string" },
          "description": { "type": "string" },
          "logo": { "type": "string", "format": "uri" },
          "image": { "type": "string", "format": "uri", "description": "1200×630 share picture" },
          "languages": { "type": "array", "items": { "type": "string" }, "description": "Languages of the whole site: interface, profile and scenario pages" },
          "chat_languages": { "type": "array", "items": { "type": "string" }, "description": "Languages a companion chats in" },
          "features": { "type": "array", "items": { "type": "string" } },
          "pricing": { "$ref": "#/components/schemas/Pricing" },
          "attribution": { "type": ["object", "null"], "properties": { "aff": { "type": "string" }, "s1": { "type": ["string", "null"] } } },
          "affiliate": { "type": "object", "properties": { "program_url": { "type": "string" }, "dashboard_url": { "type": "string" }, "param": { "type": "string" }, "sub_param": { "type": "string" }, "terms": { "type": "string" } } },
          "endpoints": { "type": "object", "additionalProperties": { "type": "string" } }
        }
      },
      "CompanionFeed": {
        "allOf": [{ "$ref": "#/components/schemas/Meta" }, { "type": "object", "properties": { "companions": { "type": "array", "items": { "$ref": "#/components/schemas/Companion" } } } }]
      },
      "ScenarioFeed": {
        "allOf": [{ "$ref": "#/components/schemas/Meta" }, { "type": "object", "properties": { "scenarios": { "type": "array", "items": { "$ref": "#/components/schemas/Scenario" } } } }]
      },
      "Companion": {
        "type": "object",
        "required": ["id", "name", "chat_url", "thumbnail"],
        "properties": {
          "id": { "type": "string" },
          "type": { "const": "companion" },
          "name": { "type": "string" },
          "platform": { "type": "string" },
          "nsfw": { "type": "boolean" },
          "age_rating": { "type": "string" },
          "fictional": { "type": "boolean", "description": "Always true: every companion is a fictional adult" },
          "gender": { "type": "string", "enum": ["female", "male"] },
          "age": { "type": "integer", "minimum": 18 },
          "city": { "type": ["string", "null"] },
          "country": { "type": ["string", "null"] },
          "occupation": { "type": ["string", "null"] },
          "tagline": { "type": "string", "description": "One line for a card" },
          "description": { "type": "string", "description": "About 150 characters, as on the profile page's meta description" },
          "about": { "type": ["string", "null"], "description": "Two or three sentences from the profile page" },
          "bio": { "type": ["string", "null"], "description": "The longer biography from the profile page, English" },
          "greeting": { "type": "string", "description": "The companion's first line" },
          "teaser": { "type": ["string", "null"], "description": "Short hook line of the card" },
          "vibe": { "type": ["string", "null"], "description": "Two-word temperament, e.g. Warm & Unhurried" },
          "starters": { "type": "array", "items": { "type": "string" }, "description": "Suggested first messages for the user" },
          "personality": { "type": ["string", "null"] },
          "tags": { "type": "array", "items": { "type": "string" } },
          "language": { "type": "string" },
          "languages": { "type": "array", "items": { "type": "string" } },
          "chat_languages": { "type": "array", "items": { "type": "string" } },
          "thumbnail": { "type": ["string", "null"], "format": "uri", "description": "Avatar, jpg" },
          "images": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "Open photo posts, jpg (the same as media.photos)" },
          "media": {
            "type": "object",
            "properties": {
              "avatar": { "$ref": "#/components/schemas/Picture" },
              "portrait": { "oneOf": [{ "$ref": "#/components/schemas/Picture" }, { "type": "null" }] },
              "cover": { "oneOf": [{ "$ref": "#/components/schemas/Picture" }, { "type": "null" }], "description": "Profile banner" },
              "photos": { "type": "array", "items": { "$ref": "#/components/schemas/Photo" }, "description": "Open posts of the companion's feed; 18+, may be explicit" },
              "videos": { "type": "array", "items": { "$ref": "#/components/schemas/Video" } }
            }
          },
          "profile_url": { "type": "string", "format": "uri" },
          "chat_url": { "type": "string", "format": "uri", "description": "Opens a chat with this companion" },
          "markdown_url": { "type": "string", "format": "uri" },
          "api_url": { "type": "string", "format": "uri" },
          "scenarios": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" }, "title": { "type": "string" }, "tagline": { "type": "string" },
                "cover": { "oneOf": [{ "$ref": "#/components/schemas/Picture" }, { "type": "null" }] },
                "scene_url": { "type": "string", "format": "uri" }, "page_url": { "type": ["string", "null"], "format": "uri" }, "api_url": { "type": "string", "format": "uri" }
              }
            }
          },
          "i18n": {
            "type": "object",
            "description": "Every site language, keyed by code",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "name": { "type": "string" }, "tagline": { "type": "string" }, "teaser": { "type": ["string", "null"] }, "vibe": { "type": ["string", "null"] },
                "description": { "type": "string" }, "greeting": { "type": ["string", "null"] }, "starters": { "type": "array", "items": { "type": "string" } },
                "city": { "type": ["string", "null"] }, "profile_url": { "type": "string", "format": "uri" }
              }
            }
          },
          "price": {
            "type": "object",
            "description": "Kept from 1.0; the feed's pricing object has the full picture",
            "properties": {
              "free": { "type": "boolean" }, "free_messages_per_day": { "type": "integer" },
              "premium_usd_month": { "type": ["number", "null"] }, "premium_usd_year": { "type": ["number", "null"] },
              "premium_messages_per_day": { "type": ["integer", "null"] }, "currency": { "type": "string" }
            }
          },
          "facts": { "type": ["object", "null"], "description": "Profile facts: age, body, ethnicity, languages, relationship, occupation, hobbies, personality, city, country" }
        }
      },
      "Scenario": {
        "type": "object",
        "required": ["id", "title", "scene_url"],
        "properties": {
          "id": { "type": "string" },
          "type": { "const": "scenario" },
          "title": { "type": "string" },
          "tagline": { "type": "string" },
          "description": { "type": "string" },
          "opening": { "type": ["string", "null"], "description": "The companion's first line in the scene" },
          "language": { "type": "string" },
          "nsfw": { "type": "boolean" },
          "age_rating": { "type": "string" },
          "fictional": { "type": "boolean" },
          "companion": {
            "type": "object",
            "properties": {
              "id": { "type": "string" }, "name": { "type": "string" }, "gender": { "type": "string" }, "age": { "type": "integer" },
              "thumbnail": { "type": ["string", "null"], "format": "uri" }, "profile_url": { "type": "string", "format": "uri" }, "api_url": { "type": "string", "format": "uri" }
            }
          },
          "cover": { "oneOf": [{ "$ref": "#/components/schemas/Picture" }, { "type": "null" }], "description": "16:9 cover" },
          "scene_url": { "type": "string", "format": "uri", "description": "Opens the chat inside this scene" },
          "page_url": { "type": ["string", "null"], "format": "uri" },
          "api_url": { "type": "string", "format": "uri" },
          "i18n": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "title": { "type": "string" }, "tagline": { "type": "string" }, "description": { "type": "string" },
                "opening": { "type": ["string", "null"] }, "page_url": { "type": ["string", "null"], "format": "uri" }
              }
            }
          }
        }
      }
    }
  }
}
