aero/v1 API reference
A REST API over everything in your academy. Available on Gold and Platinum plans. Generate keys in the app under Settings → Developer API.
Authentication
All requests carry an API key as a bearer token. Keys are shown once at creation and can be read-only or read & write; revoking a key cuts it off instantly.
curl https://clientapi.aeroapp.org/v1/courses \ -H "Authorization: Bearer aero_sk_YOUR_KEY"
| Base URL | https://clientapi.aeroapp.org/v1 |
|---|---|
| Rate limit | 120 requests per minute per key — over the limit returns 429 |
| Plans | Gold and Platinum (402 otherwise) |
| Key modes | rw full access · ro GET-only (writes return 403) |
Conventions & errors
Success responses wrap payloads in {"data": …}. Errors use WordPress REST shape:
{ "code": "aero_auth", "message": "Unknown or revoked API key.", "data": { "status": 401 } }
| Status | Meaning |
|---|---|
401 | Missing, malformed, or revoked key |
402 | Plan doesn't include this (upgrade required) |
403 | Read-only key used on a write route |
404 | Resource not found in your store |
422 | Validation failed — message explains what |
429 | Rate limited — retry after a minute |
YYYY-MM-DD HH:MM:SS.Courses
GET /courses
List courses with rollups. Query params: page, per_page (max 100), search, status (draft | published | archived).
{ "data": [ {
"id": 15, "title": "Food Hygiene Level 2", "status": "published",
"shopify_product_id": 8992883835078,
"lesson_count": 12, "enrolled": 340, "complete_pct": 71
} ], "meta": { "total": 4 } }
POST /courses
Create a draft course. Body: {"title": "New course"}. Fails with 402 at your plan's course limit.
GET /courses/{id} · PUT /courses/{id} · DELETE /courses/{id}
Read, update or delete one course. Updatable fields: title, description (HTML), image_url, status, access_settings (object — includes completion and instructor). Deleting removes content structure; enrollments, attempts and certificates are preserved as records.
GET /courses/{id}/tree
The whole course in one call: course, modules, lessons (with drip rules and SCORM status), quizzes with questions. The best starting point for read integrations.
POST /courses/{id}/duplicate
Deep-copies the course to a new draft (modules, lessons, quizzes, questions). Product link, learners and SCORM uploads are not copied.
GET /courses/{id}/reviews
Learner ratings: {"stats": {"avg": 4.8, "count": 8}, "reviews": […]}.
Modules & lessons
GET · POST /courses/{course_id}/modules
List or add modules. Create body: {"title": "Week 1", "sort_order": 0}.
PUT · DELETE /modules/{id}
GET · POST /modules/{module_id}/lessons
Create body: {"title": "Welcome", "type": "text", "content": "<p>…</p>"}. Types: text, video, audio, pdf, scorm, live. Media lessons reference an uploaded file via media_url; SCORM zips must be uploaded through the app. Live lessons take settings.live = {"start_time": "2026-09-01T18:00:00", "duration": 60, "timezone": "Europe/London"} and require a connected Zoom account — Aero creates/reschedules the meeting and emails enrolled learners.
PUT · DELETE /lessons/{id}
Update accepts title, content, media_url, sort_order, drip_rule (e.g. {"after_days": 7}, Bronze+) and settings.
Quizzes & questions
POST /courses/{course_id}/quizzes
Body: {"title": "Final exam", "module_id": null, "pass_pct": 70, "is_practice": false, "retake_limit": null}. module_id null (or omitted) makes it a course-level final.
GET · PUT · DELETE /quizzes/{id}
Update accepts title, pass_pct, time_limit_seconds (e.g. 600 for a 10-minute timer), retake_limit, is_practice, sort_order.
POST /quizzes/{id}/questions
{ "type": "mcq", "prompt": "Safe fridge temperature?",
"options": ["8°C", "5°C or below", "12°C"],
"answer": "5°C or below", "points": 1 }
answer is required for every type except essay; for multi pass an array.
Types: mcq, multi, true_false, fill_blank, essay.
DELETE /questions/{id}
Learners & enrollments
GET /people
Everyone across your academy with progress and certificate rollups.
GET /courses/{id}/students
{ "data": [ {
"enrollment_id": 88, "name": "Emma Wright", "email": "emma@acme.com",
"status": "in_progress", "pct": 40, "enrolled_at": "2026-07-20 09:14:02",
"source": "order", "cert_status": "none", "verify_code": null, "license_id": 12
} ] }
POST /courses/{id}/enroll
Manual enrollment. Body: {"learners": [{"customer_id": 712, "email": "emma@acme.com", "name": "Emma"}]}. customer_id is the numeric Shopify customer ID — create the customer in Shopify first if needed. New enrollments trigger the welcome email.
DELETE /courses/{id}/students/{enrollment_id}
Revokes access immediately. History and issued certificates remain; a repurchase reactivates.
source values now include membership — a learner who self-enrolled under a membership from their portal. Subscription-bound enrollments keep status: "active" in API responses even while portal access is paused (auto-renew off / cancelled past the paid period): the enrollment record is history, the access gate is the live subscription state. Certificates follow their own validity dates regardless of subscription state.Certificates
GET /certificates/{code}
{ "data": {
"code": "B6CB96AFBC56", "status": "valid",
"holder_name": "Tom Becker", "holder_email": "tom@acme.com",
"course_id": 15, "course_title": "Food Hygiene Level 2",
"enrollment_id": 88, "issued_at": "2026-07-12 10:00:11", "expires_at": "2027-07-03 00:00:00"
} }
POST /certificates/reissue
Body: {"enrollment_id": 88}. Voids the old code and PDF, issues a fresh code; dates are immutable. The learner re-claims their name on next visit.
Compliance & events
GET /compliance/summary
Store-wide compliance rollup — per-course totals, compliant counts and percentages, as shown on the Compliance page.
GET /events
Recent activity feed (enrollments, completions, reminders, reissues, blocks…): {"type": "…", "payload": {…}, "created_at": "…"}. Poll it to drive webhook-like automations.
Content export / import
GET /content/export
Your full course structure as portable JSON (format aero-content-v1).
POST /content/import
Body: a previously exported document. Imports everything as drafts and returns created counts. Media and SCORM are referenced, not copied.
Storefront theme data (no API key)
For showing the syllabus on the merchant's own storefront, don't use this API — two public mechanisms exist, both documented with copy-paste snippets in the guide's Theme integration section:
GET /apps/courses/api/curriculum?product_id={id}
Public JSON syllabus, fetched from theme JS on the merchant's storefront (Shopify's app proxy signs it automatically). Also accepts course_id. Published courses only; titles and structure only.
{ "data": {
"course_title": "Food Hygiene Level 2",
"modules": [ { "title": "The basics of food safety", "free_preview": true,
"lessons": [ { "title": "Welcome", "type": "text", "duration_seconds": null } ] } ],
"quizzes": [ { "title": "Final exam", "practice": false, "pass_pct": 70, "question_count": 10 } ],
"totals": { "modules": 4, "lessons": 12, "quizzes": 1, "duration_minutes": 96 }
} }
Product metafields
Every published course product carries storefront-readable metafields in the aero namespace: curriculum (json), module_count, lesson_count, quiz_count, duration_minutes — usable in Liquid (product.metafields.aero.curriculum.value) and as theme-editor dynamic sources.
Theme app blocks
Two ready-made blocks ship with the app (theme editor → product template → Add block → Apps), so most storefronts need no custom code: Course syllabus renders aero.curriculum as an accordion with type chips, durations, free-preview badges and assessments; Purchase options renders the one-time vs subscription choice per variant on themes without native selling-plan UI, keeping a hidden selling_plan input in sync with the product form — including auto-selecting the plan on subscription-only products. Reach for custom Liquid (above) only when you need a layout the blocks' settings can't express.