{
  "openapi": "3.1.0",
  "info": {
    "title": "RC Football read API",
    "version": "1.0.0",
    "description": "Flag and tackle football coaching content: 253 pages, the drill library, and the built-in formations.\n\nNo authentication, no key, no rate limit. Everything here is already public as HTML; this serves it as data so you do not have to scrape it. Responses are cached at the edge for a day, so crawl it freely.\n\nEvery play carries its diagram as data under `diagram.data`, which is the same shape the designer round-trips. That is the reason to use this rather than the markdown mirror at `<path>.md`, which can only describe a diagram in prose.\n\nThere is no write API. To put something in a coach's playbook, hand them a designer link: https://rc-football.com/docs/designer-links",
    "contact": {
      "name": "RC Football",
      "url": "https://rc-football.com/docs"
    },
    "license": {
      "name": "See the terms at /terms",
      "url": "https://rc-football.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://rc-football.com"
    }
  ],
  "tags": [
    {
      "name": "pages",
      "description": "Any page in the corpus, as structured data."
    },
    {
      "name": "collections",
      "description": "Lists, for finding out what exists."
    }
  ],
  "paths": {
    "/api/v1/pages/{path}": {
      "get": {
        "tags": [
          "pages"
        ],
        "operationId": "getPage",
        "summary": "One page of the corpus as JSON",
        "description": "Every one of the 253 pages that has a markdown mirror also answers here, resolved through the same table, so the two surfaces always cover the same paths.\n\n`path` is the page's own path without the leading slash: `/plays/5v5/mesh` is `/api/v1/pages/plays/5v5/mesh`. The home page is `index`.\n\nThree response shapes, discriminated by `kind`. `answer` is the template most of the corpus uses. `concept` is a run concept, which carries the same call drawn against two defensive answers. `static` is a hand-authored page with no content model behind it, and carries its markdown instead of fields.",
        "parameters": [
          {
            "name": "path",
            "in": "path",
            "required": true,
            "description": "The page path, no leading slash.",
            "schema": {
              "type": "string"
            },
            "examples": {
              "play": {
                "value": "plays/5v5/mesh"
              },
              "coverage": {
                "value": "coverages/cover-2"
              },
              "concept": {
                "value": "concepts/sweep"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/AnswerPage"
                        },
                        {
                          "$ref": "#/components/schemas/RunConcept"
                        },
                        {
                          "$ref": "#/components/schemas/StaticPage"
                        }
                      ],
                      "discriminator": {
                        "propertyName": "kind"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No page at that path. The hint says where the index is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/plays": {
      "get": {
        "tags": [
          "collections"
        ],
        "operationId": "listPlays",
        "summary": "Published play concepts, by format",
        "description": "One row per published concept-by-format page, with the short answer and the URLs to read more. The diagram is not inlined: fetch the page JSON for it.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Only formats with published pages, which is fewer than the designer draws.",
            "schema": {
              "type": "string",
              "enum": [
                "11v11",
                "5v5",
                "7v7",
                "nfl-flag"
              ]
            }
          },
          {
            "name": "concept",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "counter",
                "dive",
                "draw",
                "end-around",
                "flood",
                "four-verticals",
                "inside-zone",
                "iso",
                "jet-sweep",
                "mesh",
                "option",
                "outside-zone",
                "power",
                "qb-draw",
                "qb-keep",
                "reverse",
                "smash",
                "spot",
                "sweep",
                "toss",
                "trap",
                "wildcat"
              ]
            }
          },
          {
            "name": "family",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "Crossers",
                "Direct snap runs",
                "Downhill runs",
                "Gap scheme runs",
                "High-low",
                "Interior runs",
                "Misdirection runs",
                "Perimeter runs",
                "Quarterback runs",
                "Three-level stretch",
                "Triangle",
                "Vertical stretch",
                "Zone runs"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching plays, plus every value each filter accepts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "count",
                        "plays"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "formats": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "concepts": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "families": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "plays": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PlayListing"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An unknown filter value or parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/drills": {
      "get": {
        "tags": [
          "collections"
        ],
        "operationId": "listDrills",
        "summary": "The drill library, filterable",
        "description": "Filter semantics are the finder's: OR inside a group, AND across groups. Two skills widens; a skill and an age narrows.\n\nA coach arrives with constraints rather than a category, so `minutes` and `ages` are usually the useful pair.",
        "parameters": [
          {
            "name": "game",
            "in": "query",
            "required": false,
            "description": "Nine-a-side and up are tackle here.",
            "schema": {
              "type": "string",
              "enum": [
                "flag",
                "tackle"
              ],
              "default": "flag"
            }
          },
          {
            "name": "skills",
            "in": "query",
            "description": "The skill a drill teaches. Comma-separated; unknown values are dropped rather than refused, so a stale slug narrows nothing instead of failing.",
            "required": false,
            "schema": {
              "type": "string"
            },
            "examples": {
              "one": {
                "value": "catching"
              },
              "two": {
                "value": "catching,open-field"
              }
            }
          },
          {
            "name": "problems",
            "in": "query",
            "description": "The problem a drill fixes. Comma-separated; unknown values are dropped rather than refused, so a stale slug narrows nothing instead of failing.",
            "required": false,
            "schema": {
              "type": "string"
            },
            "examples": {
              "one": {
                "value": "drops"
              },
              "two": {
                "value": "drops,flags"
              }
            }
          },
          {
            "name": "ages",
            "in": "query",
            "description": "The age band it suits. Comma-separated; unknown values are dropped rather than refused, so a stale slug narrows nothing instead of failing.",
            "required": false,
            "schema": {
              "type": "string"
            },
            "examples": {
              "one": {
                "value": "5-6"
              },
              "two": {
                "value": "5-6,7-8"
              }
            }
          },
          {
            "name": "minutes",
            "in": "query",
            "required": false,
            "description": "An upper bound on ONE drill's length, not a practice budget. 15 returns everything that fits in a quarter of an hour.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 120
            }
          },
          {
            "name": "written",
            "in": "query",
            "required": false,
            "description": "true returns only drills with a setup and a coaching point.",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching drills, written ones first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "count",
                        "total",
                        "drills"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer",
                          "description": "Rows in this response."
                        },
                        "total": {
                          "type": "integer",
                          "description": "Rows matching before paging."
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "filters": {
                          "type": "object",
                          "description": "The filters as applied, after unknown values were dropped."
                        },
                        "drills": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DrillListing"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An unknown parameter, or minutes/limit/offset out of range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/formations": {
      "get": {
        "tags": [
          "collections"
        ],
        "operationId": "listFormations",
        "summary": "Built-in formations, with each alignment as play data",
        "description": "Only the formations a coach can actually pick. `alignment` is routeless on purpose: a formation is where people stand, and the routes are the play.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "5v5",
                "5v5 NFL Flag",
                "6v6",
                "7v7",
                "8v8",
                "9v9",
                "11v11"
              ]
            }
          },
          {
            "name": "side",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "offense",
                "defense"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The formations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "count",
                        "formations"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "formations": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Formation"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An unknown format or side.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "ok",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "hint",
              "docs_url"
            ],
            "properties": {
              "code": {
                "type": "string",
                "example": "NOT_FOUND"
              },
              "message": {
                "type": "string"
              },
              "details": {},
              "hint": {
                "type": "string",
                "description": "What to do next, not only what broke. Present on every error."
              },
              "docs_url": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "PlayData": {
        "type": "object",
        "description": "A play diagram as data, the same shape the designer reads and writes.\n\nRoutes are NAMED primitives rather than point lists: a route carries a `name` from the route vocabulary plus a depth and a direction, and the points are derived from those. Only a `custom` route carries explicit points.",
        "required": [
          "version",
          "mode",
          "players",
          "routes"
        ],
        "properties": {
          "version": {
            "type": "integer"
          },
          "mode": {
            "type": "string",
            "enum": [
              "offense",
              "defense"
            ]
          },
          "players": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "x": {
                  "type": "number"
                },
                "y": {
                  "type": "number"
                },
                "position": {
                  "type": "string"
                }
              }
            }
          },
          "routes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "zones": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "footballs": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "losY": {
            "type": "number"
          },
          "ballOn": {
            "type": "integer",
            "minimum": 1,
            "maximum": 99
          },
          "field": {
            "type": "string",
            "description": "Which field's true scale this is drawn at."
          }
        }
      },
      "Point": {
        "type": "object",
        "required": [
          "title",
          "body"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          }
        }
      },
      "AnswerPage": {
        "type": "object",
        "required": [
          "kind",
          "path",
          "canonical",
          "question",
          "shortAnswer"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "answer"
            ]
          },
          "path": {
            "type": "string"
          },
          "canonical": {
            "type": "string",
            "format": "uri",
            "description": "The HTML page, not this endpoint. Cite this URL."
          },
          "question": {
            "type": "string",
            "description": "The question a coach types, verbatim."
          },
          "shortAnswer": {
            "type": "string",
            "description": "The liftable paragraph. Written to stand alone without the headline."
          },
          "description": {
            "type": "string"
          },
          "author": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "credential": {
                "type": "string"
              }
            }
          },
          "published": {
            "type": "string",
            "format": "date"
          },
          "updated": {
            "type": "string",
            "format": "date"
          },
          "breadcrumbs": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "diagram": {
            "type": "object",
            "description": "Absent on a page whose answer is not one play.",
            "properties": {
              "data": {
                "$ref": "#/components/schemas/PlayData"
              },
              "alt": {
                "type": "string",
                "description": "The diagram described, generated from the play data."
              },
              "caption": {
                "type": "string"
              },
              "legend": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "designerUrl": {
                "type": "string",
                "format": "uri",
                "description": "A link to HAND A COACH, not to fetch. robots.txt disallows /designer by design, and it opens for a coach with no account."
              }
            }
          },
          "heroViews": {
            "type": "object",
            "description": "Used where the page's hero is not one diagram."
          },
          "whyItWorks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Point"
            }
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "byFormat": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "compare": {
            "type": "object",
            "description": "Two diagrams whose difference IS the answer."
          },
          "tradeoffs": {
            "type": "object",
            "description": "Competing answers, each with what it gives up."
          },
          "catalogue": {
            "type": "object",
            "description": "Several plays, each with its own diagram data."
          },
          "install": {
            "type": "object"
          },
          "adjustments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Point"
            }
          },
          "whenNotToCallIt": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Point"
            }
          },
          "defensiveAnswers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Point"
            }
          },
          "pairsWith": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "faq": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "related": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "cta": {
            "type": "object"
          },
          "sectionTitles": {
            "type": "object",
            "description": "The page's own word for a section, where it renames one."
          }
        }
      },
      "RunConcept": {
        "type": "object",
        "required": [
          "kind",
          "path",
          "slug",
          "name",
          "concept",
          "plays"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "concept"
            ]
          },
          "path": {
            "type": "string"
          },
          "canonical": {
            "type": "string",
            "format": "uri"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "concept": {
            "type": "string",
            "description": "What the concept IS. The liftable paragraph."
          },
          "reads": {
            "type": "string",
            "description": "What the ball carrier is reading."
          },
          "look": {
            "type": "string"
          },
          "finish": {
            "type": "string",
            "description": "Beat three's heading. A run has no throw."
          },
          "yards": {
            "type": "string"
          },
          "whoCanRunIt": {
            "type": "string"
          },
          "looks": {
            "type": "array",
            "description": "The two defensive answers, in the same order as `plays`.",
            "items": {
              "type": "object"
            }
          },
          "plays": {
            "type": "object",
            "description": "The same call drawn against each answer.",
            "properties": {
              "man": {
                "$ref": "#/components/schemas/PlayData"
              },
              "zone": {
                "$ref": "#/components/schemas/PlayData"
              }
            }
          },
          "assignments": {
            "type": "object",
            "description": "What each support player is FOR."
          },
          "inYourGame": {
            "type": "object",
            "description": "The same concept in flag and in tackle, where it differs."
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "StaticPage": {
        "type": "object",
        "description": "A hand-authored page. No content model behind it, so it carries its markdown.",
        "required": [
          "kind",
          "path",
          "canonical",
          "markdown"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "static"
            ]
          },
          "path": {
            "type": "string"
          },
          "canonical": {
            "type": "string",
            "format": "uri"
          },
          "markdown": {
            "type": "string"
          }
        }
      },
      "PlayListing": {
        "type": "object",
        "properties": {
          "concept": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "family": {
            "type": "string"
          },
          "format": {
            "type": "string"
          },
          "playName": {
            "type": "string"
          },
          "shortAnswer": {
            "type": "string"
          },
          "premise": {
            "type": "string"
          },
          "updated": {
            "type": "string",
            "format": "date"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "json": {
            "type": "string",
            "format": "uri",
            "description": "The whole page, diagram data included."
          },
          "markdown": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "DrillListing": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "minutes": {
            "type": "integer"
          },
          "ages": {
            "type": "string"
          },
          "players": {
            "type": "string"
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "space": {
            "type": "string"
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "setup": {
            "type": [
              "string",
              "null"
            ]
          },
          "coachingPoint": {
            "type": [
              "string",
              "null"
            ]
          },
          "hasDeepDive": {
            "type": "boolean",
            "description": "Whether the page has a progression and cues, or is a row in a finder."
          },
          "concept": {
            "type": "object"
          }
        }
      },
      "Formation": {
        "type": "object",
        "properties": {
          "format": {
            "type": "string"
          },
          "side": {
            "type": "string",
            "enum": [
              "offense",
              "defense"
            ]
          },
          "variation": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "alignment": {
            "$ref": "#/components/schemas/PlayData"
          }
        }
      }
    }
  }
}