aero
Docs / API reference

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 URLhttps://clientapi.aeroapp.org/v1
Rate limit120 requests per minute per key — over the limit returns 429
PlansGold and Platinum (402 otherwise)
Key modesrw 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 } }
StatusMeaning
401Missing, malformed, or revoked key
402Plan doesn't include this (upgrade required)
403Read-only key used on a write route
404Resource not found in your store
422Validation failed — message explains what
429Rate limited — retry after a minute
IDs are scoped to your store and stable. Timestamps are UTC, 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.

Subscriptions. Enrollment 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.

Missing an endpoint you need? Tell us at info@aeroapp.org — the API grows with real integrations.