# Heights Platform API (v1) This document describes the currently available Heights Platform JSON API endpoints that can be used by customers and integration partners. ## Base URL (multi-tenant) All customer API requests are made against the **customer’s account subdomain**: - `https://{account_subdomain}.heightsplatform.com/api/v1/...` ## Authentication Heights uses token authentication via an API key associated with an **admin user**. Send the API key in the `Authorization` header: ```text Authorization: Token token=YOUR_API_KEY ``` Bearer tokens are also accepted: ```text Authorization: Bearer YOUR_API_KEY ``` Where to get your API key: - In the app UI: **Account Settings** (shows your current user’s `api_key`). ### Admin-only API access Customer API endpoints require an API key for an admin user in the Heights account. A valid key belonging to a non-admin user returns HTTP **403 Forbidden**. ### Unauthorized responses If the token is missing or invalid, the API responds with HTTP **401 Unauthorized**. ## Content type - Responses are JSON. - Requests should use `Content-Type: application/json` when sending a JSON body. ## Response headers Authenticated v1 API responses include: - `X-Request-Id` – request identifier useful when contacting support. - `X-API-Version: v1` ## Rate limiting Public API requests are rate limited per account: - Standard accounts: **60 requests per minute** - Academy plan accounts: **300 requests per minute** The account-level limit is shared across API keys and endpoints for the same account. Requests over the limit return HTTP **429 Too Many Requests**. ## Pagination Students index is paginated by default. - `GET /students?page=N` - Page size is fixed at **50**. Orders support optional pagination. - `GET /orders?page=N&per_page=50` - `per_page` is capped at **100**. Paginated responses include a `meta` block. ```json { "meta": { "total_pages": 12, "total_students": 587 } } ``` ## Errors The API uses standard HTTP status codes and returns JSON errors. - **401**: invalid/missing API token - **403**: token is valid but not allowed, such as a non-admin API key - **404**: record not found / endpoint not available - **422**: validation error or invalid parameter - **429**: rate limit exceeded Example error response: ```json { "error": "Record not found.", "errors": [ { "code": "not_found", "message": "Record not found." } ], "request_id": "..." } ``` --- # Endpoints ## Account ### Get account and API capabilities `GET /api/v1/account` Returns account details and API capabilities for the authenticated admin key. Example response: ```json { "account": { "subdomain": "academy", "app_name": "Academy", "custom_domain": null, "currency": "USD", "locale": "en", "subscription_plan": "pro", "stripe_connected": true, "paypal_connected": false, "enrollment_open": true, "member_list_enabled": true, "points_enabled": true, "community_enabled": true }, "capabilities": { "admin_api": true, "read_courses": true, "read_bundles": true, "read_digital_products": true, "read_students": true, "manage_student_access": true, "read_orders": true, "read_order_webhook_payloads": true, "read_products": true, "manage_products": true, "read_community": true, "manage_community": true, "read_community_posts": true, "read_course_structure": true, "manage_lessons": true, "webhooks": true }, "webhook_events": [ "new_student", "new_order", "course_completed", "student_completed", "new_answer", "new_project_post" ] } ``` --- ## Courses ### List courses `GET /api/v1/courses` Returns **published** courses. Example response: ```json { "courses": [ { "id": 123, "title": "Example Course", "sold_separately": true, "price": "99.0" } ] } ``` ### Get course `GET /api/v1/courses/:id` Example response: ```json { "id": 123, "title": "Example Course", "description": "...", "slug": "example-course", "sold_separately": true, "price": "99.0", "is_published": true, "is_challenge": false, "cover_image_url": "https://...", "cover_image_thumbnail_url": "https://...", "unsplash_image_url": "https://...", "lesson_count": 12 } ``` ### List roles `GET /api/v1/courses/roles` Example response: ```json { "roles": [ { "id": 1, "name": "Gold" } ] } ``` ### Course completion percentage (for a specific student) `POST /api/v1/courses/:id/completion-percentage` Parameters: - `user_email` (string, required) Example request: ```bash curl -X POST \ -H 'Authorization: Token token=YOUR_API_KEY' \ -d 'user_email=student@example.com' \ https://{subdomain}.heightsplatform.com/api/v1/courses/123/completion-percentage ``` ### Get course structure `GET /api/v1/courses/:course_id/lessons` Returns the course outline as ordered modules and lesson summaries, plus lessons that are not assigned to a module. ```json { "course": { "id": 123, "title": "Example Course", "is_challenge": false }, "modules": [ { "id": 5, "title": "Getting Started", "sequence": 1, "is_published": true, "lessons": [ { "id": 41, "title": "Welcome", "sequence": 1, "is_published": true, "lesson_type": "Text" } ] } ], "ungrouped_lessons": [] } ``` ### Create lesson `POST /api/v1/courses/:course_id/lessons` Required parameter: `title`. Optional parameters: `content`, `module_id`, `is_published`, `lesson_type`, `points`, `sequence`, `learning_objective`, `video_embed`, `video_url`, `release_day`, and `requires_unlock`. If sequence is omitted, the lesson is placed after the existing lessons in its module or course. ```json { "title": "Welcome", "content": "Lesson body text", "module_id": 5, "is_published": false, "points": 10 } ``` ### Update lesson `PATCH /api/v1/lessons/:id` or `PUT /api/v1/lessons/:id` Only provided fields are changed. Supported fields are `title`, `content`, `is_published`, `lesson_type`, `points`, `module_id`, `sequence`, `learning_objective`, `video_embed`, `video_url`, `release_day`, and `requires_unlock`. Set `module_id` to `null` to make a lesson ungrouped. Unsupported fields return HTTP 422 with code `invalid_fields`. --- ## Bundles (Offers) ### List bundles `GET /api/v1/bundles` Example response: ```json { "bundles": [ { "id": 10, "title": "Starter Offer" } ] } ``` ### Get bundle `GET /api/v1/bundles/:id` Example response: ```json { "id": 10, "title": "Starter Offer", "description": "...", "slug": "starter-offer", "is_published": true, "cover_image_thumbnail_url": "https://...", "product_count": 3 } ``` --- ## Digital products ### List digital products `GET /api/v1/digital_products` ### Get digital product `GET /api/v1/digital_products/:id` Example response: ```json { "id": 7, "title": "Workbook PDF", "description": "...", "product_type": "download", "slug": "workbook-pdf", "price": "29.0", "is_published": true, "cover_image_url": "https://..." } ``` --- ## Unified products Use the unified products endpoints to discover, read, and update courses, challenges, digital products, bundles/offers, and projects through consistent field names. Supported `product_type` values are `course`, `challenge`, `digital_product`, `bundle`, `offer`, and `project`. `offer` is an alias for `bundle`. ### Search products `GET /api/v1/products` Optional query parameters: - `query` – title or slug search - `product_type` – one supported product type - `limit` – 1–25; default 10 ```json { "products": [ { "product_type": "course", "id": 123, "title": "Example Course", "slug": "example-course" } ], "count": 1 } ``` ### Get unified product `GET /api/v1/products/:product_type/:id_or_slug` ```json { "product": { "product_type": "course", "id": 123, "title": "Example Course", "slug": "example-course", "is_challenge": false, "attributes": { "title": "Example Course", "description": "...", "is_published": true, "price": "99.0", "hidden": false }, "truncated_attributes": [] } } ``` Long string attributes are truncated to 4,000 characters and named in `truncated_attributes`. ### Update unified product `PATCH /api/v1/products/:product_type/:id_or_slug` or `PUT /api/v1/products/:product_type/:id_or_slug` Send a `product` object containing only unified fields. Common fields include `title`, `description`, `is_published`, `price`, `hidden`, `launch_date`, `days_of_access`, `preview_page`, `allow_purchase_from_landing`, and `join_button_text`; exact support varies by product type. Projects support `title`, `description`, and `course_id`. Do not send raw model columns such as `for_sale`, `fee`, `name`, or `hidden_course`; unsupported fields return HTTP 422 with code `invalid_fields` and a list of supported fields. ```json { "product": { "title": "Updated Course", "is_published": true, "price": 129 } } ``` The API applies updates immediately. Dry-run previews are an MCP/WebMCP client feature, not a server-side API parameter. --- ## Community Channel privacy is derived from paid offers attached in Heights Platform. It cannot be set directly through these endpoints. ### Search community channels `GET /api/v1/community/channels` Optional query parameters: `query` (name or slug search) and `limit` (1–50, default 25). Only channels readable by the authenticated admin are returned. ### Get community channel `GET /api/v1/community/channels/:id_or_slug` The response includes `id`, `name`, `slug`, `description`, `locked`, group, derived `is_private`, topic count, path, and up to 10 attached offers. ### Create community channel `POST /api/v1/community/channels` ```json { "channel": { "name": "Announcements", "description": "Program updates", "group_id": 2, "locked": true } } ``` ### Update community channel `PATCH /api/v1/community/channels/:id_or_slug` or `PUT /api/v1/community/channels/:id_or_slug` Supported fields are `name`, `description`, `group_id`, and `locked`. ### List community groups `GET /api/v1/community/groups` Returns up to 100 groups ordered by position, including each group's channel count. ### Search posts in a community channel `GET /api/v1/community/channels/:id_or_slug/posts` Returns posts newest first. Each post includes its content, timestamps, topic, topic path, and author name. Posts in public channels also include a full `share_url` when public community post pages are enabled. Results are always paginated. Optional query parameters: - `query` – search post content and topic titles; maximum 200 characters - `topic_id` – return posts from one topic - `created_after` – ISO-8601 lower bound for post creation time - `created_before` – ISO-8601 upper bound for post creation time - `page` – page number; default 1 - `per_page` – page size from 1–100; default 25 ```json { "channel": { "id": 4, "name": "Announcements", "slug": "announcements" }, "posts": [ { "id": 91, "content": "Enrollment opens Friday.", "created_at": "2026-08-31T12:00:00Z", "updated_at": "2026-08-31T12:00:00Z", "published_at": null, "share_url": "https://academy.heightsplatform.com/community/announcements/september-launch", "topic": { "id": 12, "title": "September launch", "slug": "september-launch", "path": "/discuss/announcements/september-launch" }, "author": { "name": "Admin Name", "slug": "admin-name" } } ], "meta": { "current_page": 1, "per_page": 25, "total_pages": 1, "total_posts": 1 } } ``` --- ## Orders ### List orders `GET /api/v1/orders` Returns paid orders by default (`status=paid`). Each order uses the same field shape as the `new_order` webhook payload. Optional query parameters: - `status` – one of `pending`, `failed`, `paid`, `paypal_executed`, or `all` (default: `paid`) - `user_email` – exact student email match - `created_after` – ISO-8601 date/time lower bound - `created_before` – ISO-8601 date/time upper bound - `page` – enable pagination - `per_page` – page size when `page` is present, capped at 100 Example request: ```bash curl -H 'Authorization: Token token=YOUR_API_KEY' \ 'https://{subdomain}.heightsplatform.com/api/v1/orders?user_email=student@example.com&page=1&per_page=50' ``` Example response: ```json { "orders": [ { "id": "ORD_ABC123", "amount": "99.0", "currency": "USD", "description": "Course Purchase", "user_email": "student@example.com", "user_name": "Student Name", "user_first_name": "Student", "user_last_name": "Name", "status": "paid", "affiliate_id": null, "courses": [ { "id": 123, "title": "Example Course" } ], "digital_products": [], "bundles": [], "billing_city": "Austin", "billing_state": "TX", "billing_country": "US", "billing_street": "123 Main St", "billing_zip": "78701", "billing_company": null, "billing_phone": null, "created_at": "2024-01-01T12:00:00Z", "updated_at": "2024-01-01T12:00:00Z" } ], "meta": { "total_pages": 1, "total_orders": 1, "current_page": 1, "per_page": 50 } } ``` ### Get order `GET /api/v1/orders/:id` `:id` is the order’s public `unique_order` value returned as `id` by the order list and webhook payload. The response is a single order object using the same field shape as the `new_order` webhook payload. --- ## Students ### List students (paginated) `GET /api/v1/students?page=N` ### Student details (by email) `GET /api/v1/students/details?email=student@example.com` Returns: - student attributes - lesson views count - lesson completions count - paid orders - enrolled courses ### Students with 100% completion `GET /api/v1/students/completions` Returns students where `total_completion >= 100`. ### Assignment answers `GET /api/v1/students/answers` Returns all assignment answers. ### Project posts `GET /api/v1/students/project-posts` Returns all project posts. --- ## Student access management All of the following endpoints are **POST** requests under `resource :student`. ### Enroll (create or update student, grant access) `POST /api/v1/student/enroll` Parameters: - `email` (string, required) - `name` (string, required when creating a new student) - `course_id` (integer, optional) – grants access to a course by creating a $0 paid order - `bundle_id` (integer, optional) – grants access to a bundle by creating a $0 paid order ### Unenroll from paid membership `POST /api/v1/student/unenroll` Parameters: - `email` (string, required) - `name` (string, required) Effect: sets `paying_student=false`. ### Revoke course access `POST /api/v1/student/revoke-course` Parameters: - `email` (string, required) - `name` (string, required) - `course_id` (integer, required) ### Revoke bundle access `POST /api/v1/student/revoke-bundle` Parameters: - `email` (string, required) - `bundle_id` (integer, required) ### Grant role `POST /api/v1/student/grant-role` Parameters: - `email` (string, required) - `role_id` (integer, required) ### Revoke role `POST /api/v1/student/revoke-role` Parameters: - `email` (string, required) - `role_id` (integer, required) ### Reset course progress `POST /api/v1/student/reset-course-progress` Parameters: - `email` (string, required) - `course_id` (integer, required) ### Reset bundle progress `POST /api/v1/student/reset-bundle-progress` Parameters: - `email` (string, required) - `bundle_id` (integer, required) --- ## Webhooks Heights has an existing webhook system powered by the app’s webhook subscriptions. Do **not** build duplicate webhook routes for customer integrations; use the existing webhook subscription UI. ### Subscribe to webhooks 1. Sign in to the Heights account as an admin. 2. Go to **Account Settings → Integrations → Webhooks**. 3. Enter a target URL. 4. Choose an event. 5. Save the subscription. Heights will send event payloads to the configured target URL when matching events occur. ### Available events - `new_student` – triggered when a student enrolls in the program. - `new_order` – triggered when an order is generated, including paid purchases and granted access orders. - `course_completed` – triggered when a student completes a course. - `student_completed` – triggered when a student reaches 100% program completion. - `new_answer` – triggered when a student submits an assignment answer. - `new_project_post` – triggered when a student posts in a project. ### `new_order` payload The `new_order` webhook payload matches `GET /api/v1/orders/:id`. ```json { "id": "ORD_ABC123", "amount": "99.0", "currency": "USD", "description": "Course Purchase", "user_email": "student@example.com", "user_name": "Student Name", "user_first_name": "Student", "user_last_name": "Name", "status": "paid", "affiliate_id": null, "courses": [ { "id": 123, "title": "Example Course" } ], "digital_products": [], "bundles": [], "billing_city": "Austin", "billing_state": "TX", "billing_country": "US", "billing_street": "123 Main St", "billing_zip": "78701", "billing_company": null, "billing_phone": null, "created_at": "2024-01-01T12:00:00Z", "updated_at": "2024-01-01T12:00:00Z" } ``` ### Other webhook payloads `new_student`: ```json { "id": 55, "name": "Student Name", "email": "student@example.com" } ``` `course_completed`: ```json { "id": 55, "name": "Student Name", "email": "student@example.com", "course": "Example Course", "course_id": 123 } ``` `student_completed`: ```json { "id": 55, "name": "Student Name", "email": "student@example.com" } ``` `new_answer`: ```json { "id": 55, "name": "Student Name", "email": "student@example.com", "answer": "My answer text", "question": "Assignment prompt", "lesson": "Lesson Title" } ``` `new_project_post`: ```json { "id": 55, "name": "Student Name", "email": "student@example.com", "post": "Project post title" } ```