{
  "openapi": "3.1.1",
  "info": {
    "title": "Warface Lineups Public API",
    "version": "1.0.0",
    "description": "Read-only access to published lineups of every type. Create a personal token in account settings and keep it on the integrating application's server. Cross-origin browser CORS is not enabled: call this API from your backend. Never put the token in a URL, browser, public source code or logs. The limit is 120 requests per token per fixed minute; a new minute starts a separate window. Each request checks whether the token remains valid for its account. Up to 5 active tokens are allowed per account, each valid for 365 days; up to 10 creation attempts per hour. Revocation, password change/reset or ending other account sessions invalidates earlier tokens. Only published materials are returned, regardless of the token owner's role. The API returns 503 when it is temporarily unavailable. Integrations receive data only through this token-authenticated API."
  },
  "servers": [{"url": "/api/v1"}],
  "externalDocs": {"url": "/developers/en", "description": "English integration guide and selected-match map workflow"},
  "security": [{"bearerAuth": []}],
  "tags": [
    {"name": "Maps", "description": "Maps, overhead scheme images and lineups for a selected map"},
    {"name": "Lineups", "description": "All published lineups: videos, screenshots and trainer materials"},
    {"name": "Grenades", "description": "Current grenade types from the project catalogue"}
  ],
  "paths": {
    "/maps": {
      "get": {
        "operationId": "listMaps", "tags": ["Maps"], "summary": "List maps and available schemes",
        "description": "Returns maps with aliases, published lineup counts and an overhead scheme when configured. Query parameters are not supported. Store the full map id for a stable match-map association; name and aliases help with initial matching.",
        "responses": {
          "200": {"description": "Maps, including maps with no published lineups", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MapsResponse"}}}},
          "400": {"$ref": "#/components/responses/InvalidQuery"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "429": {"$ref": "#/components/responses/RateLimited"}, "503": {"$ref": "#/components/responses/Unavailable"}
        }
      }
    },
    "/maps/{map}": {
      "get": {
        "operationId": "getMap", "tags": ["Maps"], "summary": "Get a map and scheme by id, API slug or unambiguous alias",
        "parameters": [{"$ref": "#/components/parameters/MapPath"}],
        "description": "Query parameters are not supported. The scheme may be null: fall back to the lineup list. Ambiguous or unknown names return 404 instead of choosing an arbitrary map.",
        "responses": {
          "200": {"description": "The requested map", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MapResponse"}}}},
          "400": {"$ref": "#/components/responses/InvalidQuery"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}, "429": {"$ref": "#/components/responses/RateLimited"}, "503": {"$ref": "#/components/responses/Unavailable"}
        }
      }
    },
    "/maps/{map}/lineups": {
      "get": {
        "operationId": "listMapLineups", "tags": ["Maps", "Lineups"], "summary": "List all published lineups for the selected map",
        "description": "Returns one page of lineups for the selected map. Fetch its scheme separately with GET /maps/{map}. Continue requesting pages while pagination.hasNextPage=true. Lineups without marked points remain in the list: null means missing geometry, never coordinate 0. A map query parameter is not accepted because the path already selects the map.",
        "parameters": [{"$ref": "#/components/parameters/MapPath"}, {"$ref": "#/components/parameters/GrenadeFilter"}, {"$ref": "#/components/parameters/SideFilter"}, {"$ref": "#/components/parameters/PlantFilter"}, {"$ref": "#/components/parameters/MediaFilter"}, {"$ref": "#/components/parameters/Page"}, {"$ref": "#/components/parameters/PageSize"}],
        "responses": {
          "200": {"description": "A page of published lineups for the map", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LineupsResponse"}}}},
          "400": {"$ref": "#/components/responses/InvalidQuery"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}, "429": {"$ref": "#/components/responses/RateLimited"}, "503": {"$ref": "#/components/responses/Unavailable"}
        }
      }
    },
    "/grenades": {
      "get": {
        "operationId": "listGrenades", "tags": ["Grenades"], "summary": "List all grenade types",
        "description": "Use this catalogue instead of hard-coding smoke grenades as the only type. Query parameters are not supported.",
        "responses": {
          "200": {"description": "Grenade types with published lineup counts", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GrenadesResponse"}}}},
          "400": {"$ref": "#/components/responses/InvalidQuery"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "429": {"$ref": "#/components/responses/RateLimited"}, "503": {"$ref": "#/components/responses/Unavailable"}
        }
      }
    },
    "/lineups": {
      "get": {
        "operationId": "listLineups", "tags": ["Lineups"], "summary": "List published lineups with filters",
        "description": "Paginated catalogue ordered by createdAt descending, then id descending. Unknown and repeated query parameters are rejected with 400. Without filters, all grenade and material types are included, including records without schemes or points. Empty data with 200 means no materials matched; 503 is an operational failure and must not be displayed as an empty catalogue.",
        "parameters": [{"$ref": "#/components/parameters/MapFilter"}, {"$ref": "#/components/parameters/GrenadeFilter"}, {"$ref": "#/components/parameters/SideFilter"}, {"$ref": "#/components/parameters/PlantFilter"}, {"$ref": "#/components/parameters/MediaFilter"}, {"$ref": "#/components/parameters/Page"}, {"$ref": "#/components/parameters/PageSize"}],
        "responses": {
          "200": {"description": "A page of the catalogue", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LineupsResponse"}}}},
          "400": {"$ref": "#/components/responses/InvalidQuery"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}, "429": {"$ref": "#/components/responses/RateLimited"}, "503": {"$ref": "#/components/responses/Unavailable"}
        }
      }
    },
    "/lineups/{id}": {
      "get": {
        "operationId": "getLineup", "tags": ["Lineups"], "summary": "Get one published lineup with its materials",
        "description": "Use the full lineup resource id. Drafts, pending moderation, deleted materials and unknown ids are not disclosed and return 404. Query parameters are not supported.",
        "parameters": [{"$ref": "#/components/parameters/LineupPath"}],
        "responses": {
          "200": {"description": "The published lineup", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LineupResponse"}}}},
          "400": {"$ref": "#/components/responses/InvalidQuery"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}, "429": {"$ref": "#/components/responses/RateLimited"}, "503": {"$ref": "#/components/responses/Unavailable"}
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {"type": "http", "scheme": "bearer", "description": "Personal read-only token from account settings. Send it only in the Authorization: Bearer <token> header. Session cookies and query parameters cannot replace it. The complete token is shown to its owner once, when created."}
    },
    "parameters": {
      "MapPath": {"name": "map", "in": "path", "required": true, "description": "Full map id, stable API slug map-<id>, current website map slug or an unambiguous alias such as mosty, bridges or a verified native key such as ptb_bridges_up. Match values from GET /maps; do not infer native keys. URL-encode path segments and persist the full id because display names and aliases may change.", "schema": {"type": "string", "minLength": 1, "maxLength": 100}},
      "LineupPath": {"name": "id", "in": "path", "required": true, "description": "Full resource id of a published lineup", "schema": {"$ref": "#/components/schemas/ResourceId"}},
      "MapFilter": {"name": "map", "in": "query", "description": "Map id, API slug, current website map slug or unambiguous alias from GET /maps", "schema": {"type": "string", "minLength": 1, "maxLength": 100}},
      "GrenadeFilter": {"name": "grenade", "in": "query", "description": "Grenade type id, current slug, name or unambiguous transliterated name from GET /grenades", "schema": {"type": "string", "minLength": 1, "maxLength": 100}},
      "SideFilter": {"name": "side", "in": "query", "description": "Lineup side. Either filter also includes materials with side=both; unknown sides are excluded.", "schema": {"type": "string", "enum": ["attack", "defense"]}},
      "PlantFilter": {"name": "plant", "in": "query", "description": "1/2 selects a specific bomb site; 10 selects materials with no specific bomb site. The value 10 is not a wildcard; omit this filter to include all site values.", "schema": {"type": "integer", "enum": [1, 2, 10]}},
      "MediaFilter": {"name": "media", "in": "query", "description": "Stored-format filter: video means a URL is stored; screenshots means at least one frame, including a trainer frame; map means one frame plus a stored trainer recipe. Filters can overlap. This does not verify legacy material integrity: media.kind reports normalized availability after the URL/recipe is checked during projection. map does not mean marked map points; inspect placement.", "schema": {"type": "string", "enum": ["video", "screenshots", "map"]}},
      "Page": {"name": "page", "in": "query", "description": "Page number, starting at 1. Use a decimal integer without a leading zero.", "schema": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 1}},
      "PageSize": {"name": "pageSize", "in": "query", "description": "Page size. Use a decimal integer without a leading zero.", "schema": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}}
    },
    "headers": {
      "CacheControl": {"description": "Do not cache the response in HTTP proxies or browsers. An integration may keep a short backend catalogue cache and must account for unpublication.", "schema": {"type": "string", "const": "private, no-store"}},
      "NoSniff": {"schema": {"type": "string", "const": "nosniff"}},
      "RateLimit": {"description": "Maximum requests per token per fixed minute", "schema": {"type": "integer", "const": 120}},
      "RetryAfter": {"description": "Seconds until the next allowed attempt; do not retry earlier", "schema": {"type": "integer", "minimum": 1}},
      "Authenticate": {"description": "Bearer authentication is required", "schema": {"type": "string"}}
    },
    "responses": {
      "InvalidQuery": {"description": "400 INVALID_QUERY: an unknown, repeated, unsupported or malformed parameter", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "Unauthorized": {"description": "401 UNAUTHORIZED: the token is missing, invalid, expired, revoked or no longer valid for its owner's account", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}, "WWW-Authenticate": {"$ref": "#/components/headers/Authenticate"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "NotFound": {"description": "404 NOT_FOUND: the published material, map or grenade type was not found; ambiguous aliases do not select an arbitrary result", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "RateLimited": {"description": "429 RATE_LIMITED: the request limit has been reached", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}, "Retry-After": {"$ref": "#/components/headers/RetryAfter"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "Unavailable": {"description": "503 API_UNAVAILABLE: the API is temporarily unavailable. This is not an empty catalogue; use bounded exponential backoff.", "headers": {"Cache-Control": {"$ref": "#/components/headers/CacheControl"}, "X-Content-Type-Options": {"$ref": "#/components/headers/NoSniff"}, "X-RateLimit-Limit": {"$ref": "#/components/headers/RateLimit"}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
    },
    "schemas": {
      "ResourceId": {"type": "string", "pattern": "^[a-fA-F0-9]{24}$", "description": "Opaque resource identifier represented by 24 hexadecimal characters"},
      "Point": {"type": "object", "required": ["x", "y"], "properties": {"x": {"type": "number", "minimum": 0, "maximum": 1}, "y": {"type": "number", "minimum": 0, "maximum": 1}}, "description": "Point normalized to the scheme image: x increases left to right and y increases top to bottom. Apply x/y to the actual displayed image rectangle, not the outer container. With object-fit contain, add the fitted image letterbox offsets. Preserve framing, orientation and aspect ratio; do not crop or rotate independently of the markers."},
      "BombSite": {"type": "object", "required": ["id", "kind", "label", "x", "y"], "properties": {"id": {"type": "string"}, "kind": {"type": "string", "const": "site"}, "label": {"type": "string"}, "x": {"type": "number", "minimum": 0, "maximum": 1}, "y": {"type": "number", "minimum": 0, "maximum": 1}}},
      "Scheme": {"type": "object", "required": ["imageUrl", "width", "height", "coordinateSystem", "bombSites"], "properties": {"imageUrl": {"type": "string", "format": "uri", "description": "Absolute public scheme-image URL; preserve its aspect ratio and orientation"}, "width": {"type": "integer", "minimum": 1}, "height": {"type": "integer", "minimum": 1}, "coordinateSystem": {"type": "string", "const": "normalized-top-left"}, "bombSites": {"type": "array", "items": {"$ref": "#/components/schemas/BombSite"}, "description": "Bomb-site markers only; callouts and legacy spawn markers are not published"}}},
      "Reference": {"type": "object", "required": ["id", "slug", "name"], "properties": {"id": {"$ref": "#/components/schemas/ResourceId"}, "slug": {"type": "string"}, "name": {"type": "string"}}},
      "Map": {"allOf": [{"$ref": "#/components/schemas/Reference"}, {"type": "object", "required": ["aliases", "canonicalUrl", "lineupCount", "scheme"], "properties": {"aliases": {"type": "array", "items": {"type": "string"}}, "canonicalUrl": {"type": "string", "format": "uri"}, "lineupCount": {"type": "integer", "minimum": 0}, "scheme": {"anyOf": [{"$ref": "#/components/schemas/Scheme"}, {"type": "null"}], "description": "null means no scheme is configured; this is not an API error"}}}]},
      "Grenade": {"allOf": [{"$ref": "#/components/schemas/Reference"}, {"type": "object", "required": ["lineupCount"], "properties": {"lineupCount": {"type": "integer", "minimum": 0}}}]},
      "Screenshot": {"type": "object", "required": ["index", "url", "thumbnailUrl"], "properties": {"index": {"type": "integer", "minimum": 0}, "url": {"type": "string", "format": "uri"}, "thumbnailUrl": {"type": "string", "format": "uri"}}, "description": "Frames in the author's order; index starts at 0. The frame's meaning (result, position or aim) is not guaranteed and must not be invented by the integration."},
      "Video": {"type": "object", "required": ["provider", "id", "watchUrl", "embedUrl"], "properties": {"provider": {"type": "string", "const": "youtube"}, "id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{11}$"}, "watchUrl": {"type": "string", "format": "uri"}, "embedUrl": {"type": "string", "format": "uri"}}, "description": "Watch URL and standard embed URL; YouTube availability depends on the user's network. The API does not deliver a video file."},
      "Media": {"type": "object", "required": ["kind", "previewUrl", "screenshots", "screenshotCount", "screenshotsTruncated", "video"], "properties": {"kind": {"type": "string", "enum": ["video", "screenshots", "map", "unavailable"], "description": "Normalized available material after URL/recipe validation. map contains a trainer frame; recipes, 3D scenes and physical trajectories are not part of API v1."}, "previewUrl": {"type": ["string", "null"], "format": "uri", "description": "Image for a compact preview. YouTube thumbnails use the project's existing mirror; null means no preview material."}, "screenshots": {"type": "array", "maxItems": 12, "items": {"$ref": "#/components/schemas/Screenshot"}, "description": "Frames in the author's order; the response includes at most 12 frames. The current upload form accepts up to 4 frames."}, "screenshotCount": {"type": "integer", "minimum": 0, "description": "Stored frame count before the defensive limit; invalid or missing values become 0."}, "screenshotsTruncated": {"type": "boolean", "description": "true means the stored frame count exceeds the defensive limit of 12. Do not present the shortened set as complete; canonicalUrl links to the material on the site."}, "video": {"anyOf": [{"$ref": "#/components/schemas/Video"}, {"type": "null"}]}}},
      "Placement": {"type": "object", "required": ["throwPoint", "landPoint", "coordinateSystem", "connection"], "properties": {"throwPoint": {"anyOf": [{"$ref": "#/components/schemas/Point"}, {"type": "null"}], "description": "Throw location, or null when not marked"}, "landPoint": {"anyOf": [{"$ref": "#/components/schemas/Point"}, {"type": "null"}], "description": "Landing location, or null when not marked"}, "coordinateSystem": {"type": "string", "const": "normalized-top-left"}, "connection": {"type": ["string", "null"], "enum": ["straight-line-guide", null], "description": "straight-line-guide means both points are available for an illustrative connection. It is not a physically calculated trajectory, a flight recording or a bounce path; null means the pair is incomplete."}}},
      "Lineup": {"type": "object", "required": ["id", "title", "description", "canonicalUrl", "map", "grenade", "side", "typeSide", "typePlant", "typeClick", "media", "placement", "createdAt", "updatedAt"], "properties": {"id": {"$ref": "#/components/schemas/ResourceId"}, "title": {"type": "string"}, "description": {"type": "string"}, "canonicalUrl": {"type": "string", "format": "uri"}, "map": {"$ref": "#/components/schemas/Reference"}, "grenade": {"$ref": "#/components/schemas/Reference"}, "side": {"type": ["string", "null"], "enum": ["attack", "defense", "both", null], "description": "Normalized side: attack=1, defense=2, both=3; null means a legacy or unknown value. Do not assign a default side."}, "typeSide": {"type": "integer", "description": "Original project value; use side for presentation"}, "typePlant": {"type": "integer", "description": "Original bomb-site value; 1/2 means a specific site, 10 means no bomb site"}, "typeClick": {"type": "string", "description": "Stored mouse-button instruction: lc means left mouse button; rc means right mouse button. Preserve unknown values without inventing a meaning. There is no click query filter in v1; apply local presentation filters if needed."}, "media": {"$ref": "#/components/schemas/Media"}, "placement": {"$ref": "#/components/schemas/Placement"}, "createdAt": {"type": "string", "format": "date-time"}, "updatedAt": {"type": "string", "format": "date-time"}}},
      "Pagination": {"type": "object", "required": ["page", "pageSize", "total", "pages", "hasNextPage"], "properties": {"page": {"type": "integer", "minimum": 1, "maximum": 1000}, "pageSize": {"type": "integer", "minimum": 1, "maximum": 100}, "total": {"type": "integer", "minimum": 0}, "pages": {"type": "integer", "minimum": 0}, "hasNextPage": {"type": "boolean"}}, "description": "When total=0, pages=0 and data=[]. The requested page number is preserved, including beyond pages. The catalogue is not a fixed snapshot: publication or unpublication can change pages during traversal. Deduplicate by id when refreshing a backend cache."},
      "MapsResponse": {"type": "object", "required": ["data"], "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/Map"}}}},
      "MapResponse": {"type": "object", "required": ["data"], "properties": {"data": {"$ref": "#/components/schemas/Map"}}},
      "GrenadesResponse": {"type": "object", "required": ["data"], "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/Grenade"}}}},
      "LineupsResponse": {"type": "object", "required": ["data", "pagination"], "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/Lineup"}}, "pagination": {"$ref": "#/components/schemas/Pagination"}}},
      "LineupResponse": {"type": "object", "required": ["data"], "properties": {"data": {"$ref": "#/components/schemas/Lineup"}}},
      "Error": {"type": "object", "required": ["error"], "properties": {"error": {"type": "object", "required": ["code", "message"], "properties": {"code": {"type": "string", "enum": ["INVALID_QUERY", "UNAUTHORIZED", "NOT_FOUND", "RATE_LIMITED", "API_UNAVAILABLE"]}, "message": {"type": "string", "description": "Safe explanatory text; branch on code because this text may change"}}}}}
    }
  }
}
