{
  "openapi": "3.1.0",
  "info": {
    "title": "Zinstruct API",
    "version": "1.0.0",
    "description": "Read your school's data from your own tools.\n\n## Authenticating\n\nEvery request carries an API key as a bearer token:\n\n```\nAuthorization: Bearer YOUR_API_KEY\n```\n\nCreate a key at **Settings → API keys**. The full key is shown once, when you create it. Keys are read-only, reach only your own school, and stop working the moment you revoke them.\n\nStart with `GET /api/reporting/v1/health` — it proves the key and the header are right before you touch any data.\n\n## Conventions\n\n- Field names are `snake_case`; timestamps are ISO-8601 in UTC; money is always **integer cents**, never a decimal.\n- List endpoints return `{ data, total, page, page_size }`. `total` counts every matching row, not just the page.\n- Paginate with `?page=` and `?page_size=`. `page_size` is clamped to 1–100, and defaults to 50.\n- Errors return `{ \"error\": { \"code\", \"message\" } }` with a matching HTTP status. Branch on `code` — it is stable; `message` may be reworded.\n\n## Rate limits\n\nRequests are limited per key: **600 requests per 60 seconds**. Over the limit you get `429` with code `rate_limited`. Give each integration its own key so one busy tool cannot exhaust another's budget."
  },
  "servers": [
    {
      "url": "https://app.zinstruct.com",
      "description": "Your school"
    }
  ],
  "tags": [
    {
      "name": "Getting started",
      "description": "Prove your key works."
    },
    {
      "name": "Core data",
      "description": "Contacts, products, offers, and enrollments — the records your school runs on."
    },
    {
      "name": "Engagement",
      "description": "Who is learning, how far they get, and where they stop watching."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from Settings → API keys, sent as `Authorization: Bearer <key>`."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every failure uses this shape. `code` is stable and safe to branch on; `message` is for humans and may be reworded.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code.",
                "enum": [
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "bad_request",
                  "rate_limited",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation."
              }
            }
          }
        }
      },
      "Contact": {
        "type": "object",
        "description": "A person in your CRM — a lead, a member, or a customer.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable identifier."
          },
          "email": {
            "type": "string",
            "description": "Email address."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name. Falls back to first + last name when the name field is empty."
          },
          "first_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Given name."
          },
          "last_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Family name."
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own classification of this contact."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number. Falls back to the mobile number when the primary is empty."
          },
          "contact_status": {
            "type": "string",
            "description": "Lifecycle state, e.g. `active` or `archived`. Defaults to `active`."
          },
          "marketing_status": {
            "type": "string",
            "description": "Email consent, e.g. `subscribed` or `unsubscribed`. Defaults to `unsubscribed`."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the contact first came from. Immutable first-touch provenance."
          },
          "lifetime_value_cents": {
            "type": "integer",
            "description": "Total revenue attributed to this contact, in cents."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the contact was created. ISO-8601, UTC."
          },
          "last_activity_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Most recent recorded activity. ISO-8601, UTC."
          }
        }
      },
      "Product": {
        "type": "object",
        "description": "A catalog item — a course, coaching program, community, download, newsletter, or podcast.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable identifier."
          },
          "type": {
            "type": "string",
            "description": "Product type, e.g. `course`, `coaching`, `community`, `download`, `newsletter`, `podcast`."
          },
          "sub_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Further classification within the type, where one applies."
          },
          "title": {
            "type": "string",
            "description": "Product title."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product description."
          },
          "status": {
            "type": "string",
            "description": "`draft` or `published`. Defaults to `draft`."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the product was created. ISO-8601, UTC."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the product was last changed. ISO-8601, UTC."
          }
        }
      },
      "Offer": {
        "type": "object",
        "description": "The sellable pricing of a product. One product can have several offers.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable identifier."
          },
          "product_id": {
            "type": "string",
            "format": "uuid",
            "description": "The product this offer sells."
          },
          "name": {
            "type": "string",
            "description": "Offer name."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Offer description."
          },
          "price_cents": {
            "type": "integer",
            "description": "Price in the smallest currency unit. Money is always integer cents — never a float."
          },
          "currency": {
            "type": "string",
            "description": "ISO-4217 currency code, lowercase. Defaults to `usd`."
          },
          "plan": {
            "type": "string",
            "description": "`one_time` or a recurring plan. Defaults to `one_time`."
          },
          "interval": {
            "type": [
              "string",
              "null"
            ],
            "description": "Billing interval for recurring plans, e.g. `month` or `year`. Null for one-time offers."
          },
          "interval_count": {
            "type": "integer",
            "description": "How many intervals per billing cycle."
          },
          "status": {
            "type": "string",
            "description": "`draft` or `published`. Defaults to `draft`."
          },
          "pwyw": {
            "type": "boolean",
            "description": "True when the buyer names their own price."
          },
          "trial_days": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Free trial length in days, or null for no trial."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the offer was created. ISO-8601, UTC."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the offer was last changed. ISO-8601, UTC."
          }
        }
      },
      "Enrollment": {
        "type": "object",
        "description": "A contact's access to a product, with cached progress.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable identifier."
          },
          "contact_id": {
            "type": "string",
            "format": "uuid",
            "description": "The enrolled contact."
          },
          "product_id": {
            "type": "string",
            "format": "uuid",
            "description": "The product they have access to."
          },
          "granted_via_offer_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The offer that granted access, when access came from a purchase or claim."
          },
          "status": {
            "type": "string",
            "description": "Access state, e.g. `active`. Defaults to `active`."
          },
          "progress_pct": {
            "type": "number",
            "description": "Completion percentage, 0–100."
          },
          "enrolled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When access began. ISO-8601, UTC."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the product was completed, or null. ISO-8601, UTC."
          }
        }
      },
      "Course": {
        "type": "object",
        "description": "Engagement rollup for one course.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The course product id."
          },
          "title": {
            "type": "string",
            "description": "Course title."
          },
          "modules_count": {
            "type": "integer",
            "description": "Modules in the course."
          },
          "lessons_count": {
            "type": "integer",
            "description": "Lessons across all modules."
          },
          "students_enrolled": {
            "type": "integer",
            "description": "Contacts with an enrollment in this course."
          },
          "lessons_watched_over_30s": {
            "type": "integer",
            "description": "Lesson views exceeding 30 seconds of watch time."
          },
          "distinct_students_watched": {
            "type": "integer",
            "description": "Distinct students with any recorded watch time."
          },
          "avg_completion_pct": {
            "type": "number",
            "description": "Mean enrollment progress, 0–100."
          }
        }
      },
      "CourseModule": {
        "type": "object",
        "description": "Engagement rollup for one module within a course.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The module id."
          },
          "title": {
            "type": "string",
            "description": "Module title."
          },
          "lessons_count": {
            "type": "integer",
            "description": "Lessons in this module."
          },
          "lessons_watched_over_30s": {
            "type": "integer",
            "description": "Lesson views exceeding 30 seconds of watch time."
          },
          "distinct_students_watched": {
            "type": "integer",
            "description": "Distinct students with any recorded watch time in this module."
          }
        }
      },
      "Student": {
        "type": "object",
        "description": "A contact viewed through the engagement lens: activity, sessions, and revenue.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The contact id. Same identifier the public API uses for contacts."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name."
          },
          "email": {
            "type": "string",
            "description": "Email address."
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own classification of this contact."
          },
          "joined_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the contact was created. ISO-8601, UTC."
          },
          "last_active_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Most recent recorded activity. ISO-8601, UTC."
          },
          "logins_30d": {
            "type": "integer",
            "description": "Sign-ins in the last 30 days."
          },
          "avg_session_minutes": {
            "type": "number",
            "description": "Mean session length in minutes over the last 30 days, to one decimal place."
          },
          "total_session_minutes_30d": {
            "type": "integer",
            "description": "Total session minutes over the last 30 days."
          },
          "paying": {
            "type": "boolean",
            "description": "True when this contact has ever bought."
          },
          "lifetime_revenue_cents": {
            "type": "integer",
            "description": "Total revenue attributed to this contact, in cents."
          }
        }
      },
      "WatchHistoryEntry": {
        "type": "object",
        "description": "One lesson a student has watched, with how much of it.",
        "properties": {
          "course_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The course the lesson belongs to."
          },
          "course_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Course title."
          },
          "module_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The module the lesson belongs to."
          },
          "module_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Module title."
          },
          "lesson_id": {
            "type": "string",
            "format": "uuid",
            "description": "The lesson id."
          },
          "lesson_title": {
            "type": "string",
            "description": "Lesson title."
          },
          "lesson_duration_seconds": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Lesson length in seconds, when known."
          },
          "watched_seconds": {
            "type": "integer",
            "description": "Seconds of this lesson the student has watched."
          },
          "watched_pct": {
            "type": "number",
            "description": "Watched seconds as a percentage of duration, 0–100."
          },
          "watched_over_30s": {
            "type": "boolean",
            "description": "True when watch time exceeds 30 seconds."
          },
          "last_watched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Most recent watch event for this lesson. ISO-8601, UTC."
          }
        }
      },
      "RetentionPoint": {
        "type": "object",
        "description": "One point on a video retention curve.",
        "properties": {
          "second": {
            "type": "integer",
            "description": "Offset into the video, in seconds."
          },
          "viewers": {
            "type": "integer",
            "description": "Viewers still watching at this offset."
          },
          "pct": {
            "type": "number",
            "description": "Viewers as a percentage of the starting audience, 0–100."
          }
        }
      },
      "Dropoff": {
        "type": [
          "object",
          "null"
        ],
        "description": "The single largest fall between two adjacent retention points, or null when there is not enough data.",
        "properties": {
          "from_second": {
            "type": "integer",
            "description": "Offset where the fall begins."
          },
          "to_second": {
            "type": "integer",
            "description": "Offset where it ends."
          },
          "drop_pct": {
            "type": "number",
            "description": "Size of the fall in percentage points."
          }
        }
      },
      "LessonRetention": {
        "type": "object",
        "description": "Video drop-off for one lesson — where viewers stop watching.",
        "properties": {
          "lesson_id": {
            "type": "string",
            "format": "uuid",
            "description": "The lesson id."
          },
          "title": {
            "type": "string",
            "description": "Lesson title."
          },
          "course_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The course the lesson belongs to."
          },
          "module_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The module the lesson belongs to."
          },
          "duration_seconds": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Lesson length in seconds, when known."
          },
          "views": {
            "type": "integer",
            "description": "Recorded views."
          },
          "avg_watch_seconds": {
            "type": "number",
            "description": "Mean seconds watched per view."
          },
          "avg_watch_pct": {
            "type": "number",
            "description": "Mean percentage of the lesson watched, 0–100."
          },
          "retention": {
            "type": "array",
            "description": "The curve, ordered by offset.",
            "items": {
              "$ref": "#/components/schemas/RetentionPoint"
            }
          },
          "biggest_dropoff": {
            "$ref": "#/components/schemas/Dropoff"
          }
        }
      },
      "WorstRetentionLesson": {
        "type": "object",
        "description": "A lesson ranked by how early its audience falls away.",
        "properties": {
          "lesson_id": {
            "type": "string",
            "format": "uuid",
            "description": "The lesson id."
          },
          "title": {
            "type": "string",
            "description": "Lesson title."
          },
          "biggest_dropoff": {
            "$ref": "#/components/schemas/Dropoff"
          }
        }
      },
      "Summary": {
        "type": "object",
        "description": "One flat object of pre-aggregated KPIs for your school.",
        "properties": {
          "tenant": {
            "type": "string",
            "description": "Your school slug."
          },
          "students": {
            "type": "integer",
            "description": "Total contacts."
          },
          "active_students_30d": {
            "type": "integer",
            "description": "Distinct contacts with activity in the last 30 days."
          },
          "paying_customers": {
            "type": "integer",
            "description": "Contacts who have bought."
          },
          "courses": {
            "type": "integer",
            "description": "Course products."
          },
          "modules": {
            "type": "integer",
            "description": "Modules across all courses."
          },
          "lessons": {
            "type": "integer",
            "description": "Lessons across all modules."
          },
          "logins_30d": {
            "type": "integer",
            "description": "Sign-ins in the last 30 days."
          },
          "avg_session_minutes": {
            "type": "number",
            "description": "Mean session length in minutes."
          },
          "lessons_watched_over_30s_30d": {
            "type": "integer",
            "description": "Lesson views over 30 seconds in the last 30 days."
          },
          "completion_rate_pct": {
            "type": "number",
            "description": "Completed enrollments as a percentage of all enrollments, 0–100."
          }
        }
      },
      "Health": {
        "type": "object",
        "description": "Liveness probe result.",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Always true when the key authenticated."
          }
        }
      }
    }
  },
  "paths": {
    "/api/public/v1/contacts": {
      "get": {
        "tags": [
          "Core data"
        ],
        "summary": "List contacts",
        "description": "Newest first. Combine filters to narrow the list; omitted filters are not applied.",
        "operationId": "listContacts",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 1. Values below 1 are clamped to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "role",
            "in": "query",
            "required": false,
            "description": "Only contacts with this role.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only contacts with this `contact_status`, e.g. `active`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "marketing_status",
            "in": "query",
            "required": false,
            "description": "Only contacts with this email consent state, e.g. `subscribed`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of contacts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Contact"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/contacts/{id}": {
      "get": {
        "tags": [
          "Core data"
        ],
        "summary": "Get one contact",
        "operationId": "getContact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The contact.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No contact with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/products": {
      "get": {
        "tags": [
          "Core data"
        ],
        "summary": "List products",
        "description": "Newest first.",
        "operationId": "listProducts",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 1. Values below 1 are clamped to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Only products of this type, e.g. `course`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only products with this status, e.g. `published`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of products.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Product"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/products/{id}": {
      "get": {
        "tags": [
          "Core data"
        ],
        "summary": "Get one product",
        "operationId": "getProduct",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The product id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No product with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/offers": {
      "get": {
        "tags": [
          "Core data"
        ],
        "summary": "List offers",
        "description": "Newest first.",
        "operationId": "listOffers",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 1. Values below 1 are clamped to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "product_id",
            "in": "query",
            "required": false,
            "description": "Only offers selling this product.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only offers with this status, e.g. `published`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of offers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Offer"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/offers/{id}": {
      "get": {
        "tags": [
          "Core data"
        ],
        "summary": "Get one offer",
        "operationId": "getOffer",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The offer id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The offer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Offer"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No offer with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/enrollments": {
      "get": {
        "tags": [
          "Core data"
        ],
        "summary": "List enrollments",
        "description": "Most recently enrolled first.",
        "operationId": "listEnrollments",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 1. Values below 1 are clamped to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "product_id",
            "in": "query",
            "required": false,
            "description": "Only enrollments in this product.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only enrollments belonging to this contact.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only enrollments with this status, e.g. `active`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of enrollments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Enrollment"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/enrollments/{id}": {
      "get": {
        "tags": [
          "Core data"
        ],
        "summary": "Get one enrollment",
        "operationId": "getEnrollment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The enrollment id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The enrollment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Enrollment"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No enrollment with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/courses": {
      "get": {
        "tags": [
          "Engagement"
        ],
        "summary": "List courses with engagement",
        "description": "Every course with its structure counts, enrollment count, watch activity, and mean completion. Watch metrics read a materialized table and stay at 0 until viewing is recorded.",
        "operationId": "listCourses",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 1. Values below 1 are clamped to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of courses.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Course"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/courses/{course_id}/modules": {
      "get": {
        "tags": [
          "Engagement"
        ],
        "summary": "List a course’s modules with engagement",
        "description": "Modules in their display order, each with lesson count and watch activity.",
        "operationId": "listCourseModules",
        "parameters": [
          {
            "name": "course_id",
            "in": "path",
            "required": true,
            "description": "The course product id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 1. Values below 1 are clamped to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of modules.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CourseModule"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No course with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/students": {
      "get": {
        "tags": [
          "Engagement"
        ],
        "summary": "List students with activity",
        "description": "Contacts with sign-in, session, and revenue metrics attached.",
        "operationId": "listStudents",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 1. Values below 1 are clamped to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "role",
            "in": "query",
            "required": false,
            "description": "Only students with this role.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Pass `last_active_at` to order by most recently active.",
            "schema": {
              "type": "string",
              "enum": [
                "last_active_at"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of students.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Student"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/students/{student_id}": {
      "get": {
        "tags": [
          "Engagement"
        ],
        "summary": "Get one student",
        "operationId": "getStudent",
        "parameters": [
          {
            "name": "student_id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The student.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Student"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No student with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/students/{student_id}/watch-history": {
      "get": {
        "tags": [
          "Engagement"
        ],
        "summary": "List what a student has watched",
        "description": "Every lesson with recorded watch time for this student, newest first.",
        "operationId": "listStudentWatchHistory",
        "parameters": [
          {
            "name": "student_id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, starting at 1. Values below 1 are clamped to 1.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Rows per page. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of watch history.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WatchHistoryEntry"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No student with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/lessons": {
      "get": {
        "tags": [
          "Engagement"
        ],
        "summary": "Rank lessons by earliest drop-off",
        "description": "Lessons ordered by how early their audience falls away — earliest drop first, larger drops breaking ties. Lessons with no retention data are excluded. This endpoint takes `limit` rather than `page_size`, and always reports page 1.",
        "operationId": "listWorstRetentionLessons",
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Only `worst_retention` is supported. Any other value returns 400.",
            "schema": {
              "type": "string",
              "enum": [
                "worst_retention"
              ],
              "default": "worst_retention"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many lessons to return. Clamped to the 1–100 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The ranked lessons.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "total",
                    "page",
                    "page_size"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorstRetentionLesson"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total rows matching the filters, across all pages."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "page_size": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An unsupported `sort` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/lessons/{lesson_id}/retention": {
      "get": {
        "tags": [
          "Engagement"
        ],
        "summary": "Get a lesson’s retention curve",
        "description": "The full drop-off curve for one lesson, plus the single biggest fall.",
        "operationId": "getLessonRetention",
        "parameters": [
          {
            "name": "lesson_id",
            "in": "path",
            "required": true,
            "description": "The lesson id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The curve.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LessonRetention"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No lesson with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/summary": {
      "get": {
        "tags": [
          "Engagement"
        ],
        "summary": "Get the KPI summary",
        "description": "A single flat object of headline numbers for your school — the cheapest call for a dashboard tile.",
        "operationId": "getSummary",
        "responses": {
          "200": {
            "description": "The summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Summary"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The summary could not be built.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/reporting/v1/health": {
      "get": {
        "tags": [
          "Getting started"
        ],
        "summary": "Check a key works",
        "description": "Returns `{ \"ok\": true }` when the key authenticated. The right first call when wiring up an integration: it proves the key and the header are correct without touching any data.",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "The key is valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "401": {
            "description": "The token is missing, malformed, invalid, or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}