Sporty's Partner API

Partner API · Integration guide

Build Sporty's courses into your flight-school platform

The Partner API connects your flight-school software to Sporty's online ground school. Use it to give your students access to Sporty's courses, keep your flight schools in sync, follow each student's progress, and get notified when they reach a milestone.

How it fits together

Your server talks to Sporty's in two ways. For anything about your account (your courses, your schools, the licenses you sell) it uses a partner token. To read one student's data it uses a student token, which you get after that student signs in with Sporty's and approves your app. Sporty's calls you back with webhooks.

How a partner integration talks to Sporty's Your server calls the Sporty's Partner API with a partner token for courses, schools and licenses, and with a student token for a student's profile and progress. Students sign in with Sporty's to approve your app. Sporty's sends webhooks to your server. Your platform Server with your client ID and client secret Sporty's /oauth2/token /oauth2/authorize (student sign-in) /oauth2/api/partner/… /oauth2/api/me /oauth2/api/courses/{id}/progress Partner token: courses, schools, licenses Student token: profile and progress Webhooks: lesson, course, exam events Students approve your app once by signing in to their Sporty's account.
Everything is HTTPS and JSON. API calls go from your server to Sporty's; only the student's sign-in happens in a browser.

Before you start

Get your credentials

Sporty's sets up your partner account, chooses the courses you can offer, and sends you a client ID and client secret. Your client ID identifies you on every call: you only ever see your own schools, courses, licenses and webhooks.

If you plan to connect student accounts, also send us the redirect URL(s) where students land after they approve your app, for example https://app.example.com/sportys/callback. Sporty's matches them exactly, so include the scheme, host and path you'll really use. Students see your company name and logo when they approve your app.

Keep the client secret on your server

Never put it in a mobile app, a browser, or a public repository. If it leaks, contact Sporty's and we'll issue a new one. The old secret stops working as soon as we do.

Base URL

https://courses.sportys.com/altitude

Every path in this guide goes after the base URL. For example, the token endpoint is https://courses.sportys.com/altitude/oauth2/token.

Conventions

Quickstart

These four calls take you from credentials to a student with a course.

  1. Get a partner access token

    Exchange your client ID and secret for an access token.

    Request
    curl -X POST "https://courses.sportys.com/altitude/oauth2/token" \
      -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
      -d "grant_type=client_credentials"
    Response 200
    {
      "access_token": "eyJraWQiOiJ...",
      "token_type": "Bearer",
      "expires_in": 3599
    }

    Send the token on every other call as Authorization: Bearer <access_token>.

  2. List the courses you can offer

    Request
    curl "https://courses.sportys.com/altitude/oauth2/api/partner/courses" \
      -H "Authorization: Bearer $TOKEN"

    Each item's webAppId is the course ID you'll use in the next steps.

  3. Register a flight school

    Request
    curl -X POST "https://courses.sportys.com/altitude/oauth2/api/partner/school" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "partnerFlightSchoolId": "fs-10482",
        "companyName": "Blue Sky Aviation",
        "isTestAccount": false,
        "enabledByUserEmail": "ops@blueskyaviation.example",
        "event": "STATUS_ENABLED"
      }'
  4. Give a student a course

    Request
    curl -X POST "https://courses.sportys.com/altitude/oauth2/api/partner/purchase" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "sportysId": 1234567,
        "purchases": [
          {
            "webAppId": "PRIVATE",
            "purchaseId": "order-58213-1",
            "partnerFlightSchoolId": "fs-10482"
          }
        ]
      }'

    A 201 Created with an empty body means it worked: the student has the course. Next, read Give a student access for the details, or set up webhooks.

Key concepts

TermWhat it meansIdentified by
PartnerYour company. Everything you create belongs to your partner account.Your OAuth client ID
CatalogThe courses Sporty's has enabled for your account. You can only sell and look up these.A list of course IDs
SchoolA flight school that uses your platform.partnerFlightSchoolId, an ID you choose
StudentA person with a Sporty's account.sportysId, the Sporty's customer number
CourseA Sporty's course, such as Private Pilot. A course has volumes (sections), and each volume has elements: videos, interactive lessons, quizzes and documents.webAppId, for example PRIVATE
LicenseA student's access to a course for a set term. You create one by reporting a purchase.purchaseId, an ID you choose for each sale
Connected studentA student who signed in with Sporty's and approved your app. You can read their profile and progress, and you get webhooks about them.A student access token

Authentication

The API uses OAuth 2.0. Every call needs an access token in the Authorization header. Which token you need depends on whose data you're touching:

Partner tokenStudent token
Use it forYour courses, schools, licenses and webhooksOne student's profile and course progress
Endpoints/oauth2/api/partner/…/oauth2/api/me
/oauth2/api/courses/{webAppId}/progress
OAuth flowClient credentialsAuthorization code, with PKCE
Who approvesNobody: your server asks directlyThe student, once, by signing in to Sporty's

Both flows use the same token endpoint, https://courses.sportys.com/altitude/oauth2/token. OpenID Connect discovery is at https://courses.sportys.com/altitude/.well-known/openid-configuration if your OAuth library can use it.

Get a partner token

POST/oauth2/token

Authenticate with HTTP Basic auth (client ID as the username, client secret as the password) and send grant_type=client_credentials as a form field, as in the quickstart. If Sporty's set your account up for client_secret_post, send client_id and client_secret as form fields instead.

Connect a student

To read a student's profile or progress, or to get webhooks about them, the student has to approve your app. This is the standard OAuth 2.0 authorization code flow, so most OAuth client libraries handle it for you.

  1. Send the student to Sporty's

    Create a random state and a PKCE code_verifier, then redirect the student's browser to:

    Authorization URL
    https://courses.sportys.com/altitude/oauth2/authorize
      ?response_type=code
      &client_id=YOUR_CLIENT_ID
      &redirect_uri=https%3A%2F%2Fapp.example.com%2Fsportys%2Fcallback
      &scope=openid%20profile
      &state=9f2c4e81a07b4d3c8e5f6a1b2c3d4e5f
      &code_challenge=BASE64URL_SHA256_OF_VERIFIER
      &code_challenge_method=S256

    Ask for openid profile. profile lets you read /oauth2/api/me, and the approval itself is what turns on webhooks for this student.

    Use letters and digits in state

    Use a value like 32 random hex characters. Values that contain - or _, such as UUIDs or base64url strings, make the sign-in fail before the student is sent back to you.

  2. The student signs in and approves

    The student logs in with their Sporty's account and approves your app; they only see this screen the first time. Sporty's then redirects to your redirect_uri with ?code=…&state=…. Check that state matches what you sent.

    If the student cancels, Sporty's doesn't redirect back to you at all, so don't wait on a callback forever.

  3. Exchange the code for tokens

    Do this right away: codes expire after a few minutes.

    Request
    curl -X POST "https://courses.sportys.com/altitude/oauth2/token" \
      -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
      -d "grant_type=authorization_code" \
      -d "code=CODE_FROM_CALLBACK" \
      -d "redirect_uri=https://app.example.com/sportys/callback" \
      -d "code_verifier=YOUR_CODE_VERIFIER"

    The response has an access_token, a refresh_token and an id_token. Store the refresh token securely against the student's record in your system.

  4. Call the student endpoints, and refresh when needed

    Use the access token on /oauth2/api/me and /oauth2/api/courses/{webAppId}/progress. When it expires, get a new one without involving the student:

    Request
    curl -X POST "https://courses.sportys.com/altitude/oauth2/token" \
      -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
      -d "grant_type=refresh_token" \
      -d "refresh_token=STORED_REFRESH_TOKEN"

    The refresh token stays the same each time you use it, until it expires. If a refresh fails with invalid_grant, the refresh token expired or the student disconnected your app: ask them to connect again.

Optional: add the student to one of your schools

Instead of a random state, you can send a JSON object encoded in standard Base64 (not base64url) with your partnerFlightSchoolId as schoolId. Put a random value in it too, so state still protects the sign-in. When the student approves, Sporty's adds them to that school, as long as you've registered the school and Sporty's has linked it to a school on Sporty's.

state before encoding
{ "schoolId": "fs-10482", "nonce": "9f2c4e81a07b4d3c8e5f6a1b2c3d4e5f" }

URL-encode the Base64 string when you put it in the authorization URL, because it can contain +, / and =.

Show your course catalog

Use these two calls to list the courses you can offer and to show what's inside each one, for example to build a course picker or to link a student straight to a lesson.

List your courses

GET/oauth2/api/partner/coursesPartner token
Response 200
[
  {
    "webAppId": "PRIVATE",
    "shortName": "Private Pilot",
    "totalElements": 170,
    "totalRequiredElements": 133,
    "courseLastUpdateDateTime": "2026-07-16T19:30:10.959Z",
    "elementsLastUpdateDateTime": "2026-06-16T17:46:05.123Z",
    "metadata": {
      "url": "https://courses.sportys.com/training/portal/course/PRIVATE"
    }
  },
  {
    "webAppId": "INSTRUMENT",
    "shortName": "Instrument Rating",
    "totalElements": 105,
    "totalRequiredElements": 87,
    "courseLastUpdateDateTime": "2026-07-16T19:30:10.888Z",
    "elementsLastUpdateDateTime": "2026-04-22T01:46:08.649Z",
    "metadata": {
      "url": "https://courses.sportys.com/training/portal/course/INSTRUMENT"
    }
  }
]
FieldTypeDescription
webAppIdstringThe course ID. Use it everywhere a course is needed.
shortNamestringDisplay name, for example "Private Pilot".
totalElementsintegerNumber of lessons (elements) in the course.
totalRequiredElementsintegerHow many of those count toward completion. Optional lessons don't.
courseLastUpdateDateTimedate-timeWhen the course details last changed.
elementsLastUpdateDateTimedate-timeWhen the course's lessons last changed. Re-fetch the course details if this is newer than your copy.
metadata.urlstringLink to the course on Sporty's.

The list isn't in any particular order. Sort it however suits your UI.

Get one course with its lessons

GET/oauth2/api/partner/courses/{webAppId}Partner token

Returns the same summary plus volumes, each with its elements. It covers the course's video training volumes.

Response 200 (shortened)
{
  "webAppId": "PRIVATE",
  "shortName": "Private Pilot",
  "totalElements": 170,
  "totalRequiredElements": 133,
  "courseLastUpdateDateTime": "2026-07-16T19:30:10.959Z",
  "elementsLastUpdateDateTime": "2026-06-16T17:46:05.123Z",
  "metadata": { "url": "https://courses.sportys.com/training/portal/course/PRIVATE" },
  "volumes": [
    {
      "uuid": "1a3ff1fa-1d1f-4528-9a71-5e1ebee8d88f",
      "description": "Your First Few Hours",
      "sectionId": "1",
      "order": 3,
      "totalElements": 26,
      "totalRequiredElements": 26,
      "elements": [
        {
          "uuid": "8af588a3-6bd4-4525-9fb9-ddb248d6eed8",
          "type": "Video",
          "name": "Getting Started with Sporty's Learn to Fly Course",
          "required": true,
          "order": 0,
          "metadata": {
            "runtime": "00:05:27",
            "runtimeSeconds": 327,
            "url": "https://courses.sportys.com/training/portal/course/PRIVATE/volumes/VIDEO_TRAINING/1a3ff1fa-1d1f-4528-9a71-5e1ebee8d88f/8af588a3-6bd4-4525-9fb9-ddb248d6eed8"
          }
        }
      ]
    }
  ]
}

If the course doesn't exist you get 404. If it exists but isn't in your catalog you get 401 with a message; see Errors.

Keep schools in sync

Register each flight school on your platform with Sporty's, using your own ID for it. You'll use that ID when you give its students a course.

Create or update a school

POST/oauth2/api/partner/schoolPartner token

Posting a partnerFlightSchoolId you've already used updates that school, so use this call for both creating and updating. It returns 200 with an empty body.

FieldTypeDescription
partnerFlightSchoolIdstringRequiredYour ID for the school.
companyNamestringRequiredThe school's name.
enabledByUserEmailstringRequired*Email of the person at the school who turned the integration on. *Send this or userEmail.
userEmailstringOptionalUsed as enabledByUserEmail when that's empty.
isTestAccountbooleanOptionalMarks a school you use for testing. Send it on every update: leaving it out sets it to false.
eventstringOptionalSTATUS_ENABLED or STATUS_DISABLED. Leave it out to keep the current status. New schools start enabled.

Disabling a school marks it inactive. It doesn't change any student's access.

List your schools

GET/oauth2/api/partner/schoolPartner token
Response 200
[
  {
    "id": 42974776,
    "partnerFlightSchoolId": "fs-10482",
    "schoolId": 42908793,
    "companyName": "Blue Sky Aviation",
    "isTestAccount": false,
    "enabledByUserEmail": "ops@blueskyaviation.example",
    "createDate": "2026-01-21T18:54:24.058Z",
    "schoolCode": "BLUE_SKY",
    "partnerSchoolStatus": "ENABLED"
  },
  {
    "id": 43857884,
    "partnerFlightSchoolId": "fs-20931",
    "schoolId": null,
    "companyName": "UNKNOWN",
    "isTestAccount": false,
    "enabledByUserEmail": "UNKNOWN",
    "createDate": "2026-08-03T15:53:13.019Z",
    "schoolCode": null,
    "partnerSchoolStatus": "ENABLED"
  }
]

Give a student access to a course

When a student buys a Sporty's course through you, call this to give them a license. One call can grant several courses.

POST/oauth2/api/partner/purchasePartner token

Identify the student

The student needs a Sporty's account. Identify them in one of two ways:

SendWhen
sportysIdPreferred. You get it from /oauth2/api/me once the student has connected their account. If you send it, email is ignored.
email, firstName, lastNameWhen the student hasn't connected yet. The email must match the one on their Sporty's account exactly. Include the name too: Sporty's needs it the first time a student uses its courses.

Request

FieldTypeDescription
sportysIdintegerOne ofThe student's Sporty's customer number.
emailstringOne ofThe email on the student's Sporty's account.
firstName, lastNamestringWith emailThe student's name.
purchasesarrayRequiredOne entry per course. At least one.
purchases[].webAppIdstringRequiredThe course to grant. It must be in your catalog.
purchases[].purchaseIdstringRequiredYour unique ID for this sale. Reuse it if you retry.
purchases[].partnerFlightSchoolIdstringRecommendedThe school the student bought through. Register the school first.
purchases[].datedate-timeOptionalWhen the license starts. Defaults to now. A future date means the student can't use the course until then.
Request body
{
  "sportysId": 1234567,
  "purchases": [
    {
      "webAppId": "PRIVATE",
      "purchaseId": "order-58213-1",
      "partnerFlightSchoolId": "fs-10482"
    },
    {
      "webAppId": "INSTRUMENT",
      "purchaseId": "order-58213-2",
      "partnerFlightSchoolId": "fs-10482"
    }
  ]
}
Request body
{
  "email": "jordan.rivera@example.com",
  "firstName": "Jordan",
  "lastName": "Rivera",
  "purchases": [
    {
      "webAppId": "PRIVATE",
      "purchaseId": "order-58213-1",
      "partnerFlightSchoolId": "fs-10482",
      "date": "2026-11-12T15:00:00Z"
    }
  ]
}

What happens

Retries and duplicates

StatusMeaning
201Done. Empty body.
400The body is invalid, for example purchases is empty or email isn't an email address.
401 with a messageA course isn't in your catalog. With an empty body, your token is missing or expired.
404No student with that sportysId, or no course with that webAppId.
409A purchaseId is already used for a different student.

Report a purchase for a student who isn't connected

POST/oauth2/api/partner/report-purchasePartner token

Use this when a student buys a Sporty's course through you but hasn't connected a Sporty's account. It records the sale with Sporty's. It doesn't create an account or give the student access; to do that, use /partner/purchase.

FieldTypeDescription
courseIdstringRequiredThe course that was bought (a webAppId in your catalog).
purchaseIdstringRequiredYour unique ID for this sale.
companyIdstringOptionalYour ID for the school the student bought through.
userobjectOptionalThe student as they appear in your system: id, studentId, email, firstName, lastName.
Request body
{
  "courseId": "PRIVATE",
  "purchaseId": "order-58214-1",
  "companyId": "fs-10482",
  "user": {
    "id": "u-88412",
    "studentId": "S-2026-0193",
    "email": "sam.patel@example.com",
    "firstName": "Sam",
    "lastName": "Patel"
  }
}

It returns 200 with an empty body. Send each sale once: Sporty's doesn't combine repeated reports. Errors are the same as for purchases: 404 for an unknown course, 401 for a course outside your catalog.

Show a student's progress

Once a student has connected their account, use their student token to read who they are and how far along they are. Neither call accepts a partner token.

Get the student's profile

GET/oauth2/api/meStudent token · profile scope
Response 200
{
  "id": 50211873,
  "sportysId": 1234567,
  "email": "jordan.rivera@example.com",
  "firstName": "Jordan",
  "lastName": "Rivera",
  "createdDate": "2026-03-02T14:11:09Z",
  "address": {
    "address": null,
    "city": null,
    "state": null,
    "zip": null
  },
  "activeLicenses": ["PRIVATE", "SPORT"],
  "inactiveLicenses": []
}
FieldDescription
sportysIdThe student's Sporty's customer number. Save it on your student record: it's how you give this student a course, and it's the userId in webhooks.
activeLicensesCourse IDs the student can use now, however they got them. A course can appear more than once.
inactiveLicensesCourse IDs whose license has ended.
addressPostal address, if the student gave one. Any part can be null.

Get progress in one course

GET/oauth2/api/courses/{webAppId}/progressStudent token
Response 200 (shortened)
{
  "completedElements": [
    "8af588a3-6bd4-4525-9fb9-ddb248d6eed8",
    "9b31980f-e74c-42e4-b47c-590b95d458a3"
  ],
  "tests": [
    {
      "id": 61540032,
      "uuid": "5b0e7d0c-2c1f-4a51-9d0e-3f1c2b7a9e41",
      "completedDate": "2026-08-03T15:57:02.668Z",
      "questions": 60,
      "correct": 55,
      "grade": 92,
      "pass": true
    }
  ],
  "milestones": [],
  "courseRequirement": {
    "passingGrade": 80,
    "passingTests": 2
  },
  "certificates": [],
  "completion": {
    "percentComplete": 24,
    "requiredPercentComplete": 31,
    "videoPercentComplete": 24,
    "requiredVideoPercentComplete": 31,
    "displayedPercentComplete": 31,
    "videoWatchedLength": { "hours": 4, "minutes": 12, "seconds": 37 },
    "elements": {
      "elements": 170,
      "requiredElements": 133,
      "completedElements": 41,
      "completedRequiredElements": 41
    }
  }
}

How to use it:

A student without a license for the course still gets 200, with the license-based figures at zero. An unknown webAppId returns 404.

Don't poll for changes

Use webhooks to hear about finished lessons and courses, then fetch progress for that one student.

Webhooks

Webhooks tell your server when something happens to a student: a course was bought, a lesson or course was finished, or the student became ready for the FAA written exam. Sporty's sends an HTTPS POST with a small JSON body to the URL you register.

Which students you hear about

You get events for students who have connected their Sporty's account to your app, about courses in your catalog. Events from before a student connected aren't sent afterwards, and accounts registered as flight instructors (CFIs) don't generate events.

Set up

  1. Choose how Sporty's proves it's Sporty's

    Pick a long random secret and tell Sporty's how to send it. One setting covers all your webhooks. Sporty's doesn't send any events until it's set.

    PUT/oauth2/api/partner/webhook-configPartner token
    Request body
    {
      "authenticationType": "BEARER_TOKEN",
      "tokenValue": "whsec_4f9a1c7e2b8d6a3f5e0c9b7d1a2e4f6c"
    }
    authenticationTypeHeader on each webhook
    BEARER_TOKENAuthorization: Bearer <tokenValue>
    API_KEYx-api-key: <tokenValue>
    CUSTOM_HEADER<customHeaderName>: <tokenValue>. Also send customHeaderName.

    Check that header on every request and reject anything that doesn't match. GET /oauth2/api/partner/webhook-config shows the current setting.

  2. Register a URL for each event you want

    POST/oauth2/api/partner/webhooksPartner token
    Request body
    {
      "eventType": "COURSE_COMPLETED",
      "url": "https://app.example.com/sportys/webhooks",
      "enabled": true
    }
    Response 200
    {
      "id": 812,
      "eventType": "COURSE_COMPLETED",
      "url": "https://app.example.com/sportys/webhooks",
      "enabled": true
    }
    • Set enabled to true. If you leave it out, the webhook is created switched off.
    • One webhook per event type. The same URL can take every event; use the event field to tell them apart. If you register the same event twice, you get every event twice.
    • Use an HTTPS URL.

Change or remove a webhook

CallWhat it does
GET /oauth2/api/partner/webhooksLists your webhooks.
PUT /oauth2/api/partner/webhooks/{webhookId}Replaces the webhook. Send all three fields: a missing enabled switches it off. Returns the updated webhook.
DELETE /oauth2/api/partner/webhooks/{webhookId}Removes it and cancels its pending retries. Returns 204.

Events

EventSent when
COURSE_PURCHASEDThe student gets a license for the course, whether through you, Sporty's website or Sporty's app.
LESSON_COMPLETEDThe student finishes a required lesson: watches a required video, completes a required interactive lesson or document, or passes a quiz. eventDetails says which.
COURSE_COMPLETEDThe student finishes the course's last required lesson.
WRITTEN_EXAM_ELIGIBLEThe student has finished the course and passed the required practice tests, so they can ask for their FAA written exam endorsement.
WRITTEN_EXAM_REQUESTEDThe student asks for that endorsement.
TESTSporty's sends a test on request. See Test your endpoint.

COURSE_PURCHASED, COURSE_COMPLETED, WRITTEN_EXAM_ELIGIBLE and WRITTEN_EXAM_REQUESTED each happen once per student and course.

Payload

What your server receives
POST /sportys/webhooks HTTP/1.1
Host: app.example.com
Content-Type: application/json; charset=utf-8
Authorization: Bearer whsec_4f9a1c7e2b8d6a3f5e0c9b7d1a2e4f6c

{
  "sportysId": "1234567",
  "userId": "1234567",
  "webAppId": "PRIVATE",
  "courseId": "PRIVATE",
  "event": "LESSON_COMPLETED",
  "timestamp": "2026-11-12T15:04:05.482Z",
  "eventDetails": {
    "elementUuid": "9b31980f-e74c-42e4-b47c-590b95d458a3",
    "elementType": "Video",
    "elementTitle": "When Should You Fly?"
  }
}
FieldDescription
eventThe event type.
userIdThe student's Sporty's customer number (sportysId), as a string.
courseIdThe course ID.
timestampWhen it happened, in UTC. A retried delivery keeps the original time.
eventDetailsFor LESSON_COMPLETED: the lesson's elementUuid, elementType and elementTitle. null for every other event.
sportysId, webAppIdOlder copies of userId and courseId, kept for existing integrations. Use userId and courseId.

Delivery and retries

Test your endpoint

Register a webhook with "eventType": "TEST" and ask your Sporty's contact to send a test event. It arrives with your authentication header and this body:

Test event body
{ "event": "TEST", "timestamp": "2026-11-12T15:04:05.482Z" }

A failed test is retried like any other event, so disable or delete the TEST webhook when you're done.

Errors

Errors use standard HTTP status codes. Most come with a JSON body (application/problem+json) that has a status and a title or message. Authentication failures come with an empty body.

StatusWhat it meansWhat to do
400The request body is invalid: a required field is missing, the JSON is malformed, or a value isn't allowed. Validation errors list the fields in fieldErrors.Fix the request. Don't retry it unchanged.
401, empty bodyThe access token is missing, expired or no longer accepted.Get a new token and retry once.
401 with a messageThe course isn't in your catalog.Check the webAppId, or ask Sporty's to add the course.
403Wrong kind of token, such as a student token on a /partner/ endpoint, a partner token on /me, or a POST sent without a token.Use the token type the endpoint needs.
404The student, course or webhook doesn't exist.Check the ID.
409The purchaseId is already used for a different student.Use a new purchaseId for this sale.
5xxSporty's couldn't complete the request. Some invalid requests also return 500 today.Check the request against this guide, then retry with backoff. Retrying a purchase with the same purchaseIds is safe.
404
{
  "type": "https://www.jhipster.tech/problem/entity-not-found",
  "title": "Course not found for web app id: PRIVTE",
  "status": 404
}
401
{
  "type": "https://www.jhipster.tech/problem/problem-with-message",
  "status": 401,
  "path": "/altitude/oauth2/api/partner/purchase",
  "message": "Licenses cannot be submitted for the course MULTIENGINE"
}
400
{
  "type": "https://www.jhipster.tech/problem/constraint-violation",
  "title": "Method argument not valid",
  "status": 400,
  "path": "/altitude/oauth2/api/partner/purchase",
  "message": "error.validation",
  "fieldErrors": [
    { "objectName": "partnerPurchaseRequest", "field": "purchases", "message": "must not be empty" }
  ]
}

Match on the status code, not the message text: messages can change.

All endpoints

Paths are relative to https://courses.sportys.com/altitude. The API reference has every field, and the OpenAPI file imports into Postman or Insomnia.

MethodPathTokenWhat it doesSuccess
POST/oauth2/tokenClient ID and secretGet an access token200
GET/oauth2/api/partner/coursesPartnerList the courses you can offer200
GET/oauth2/api/partner/courses/{webAppId}PartnerOne course with its volumes and lessons200
GET/oauth2/api/partner/schoolPartnerList your schools200
POST/oauth2/api/partner/schoolPartnerCreate or update a school200, empty body
POST/oauth2/api/partner/purchasePartnerGive a student access to one or more courses201, empty body
POST/oauth2/api/partner/report-purchasePartnerRecord a sale for a student who isn't connected200, empty body
GET/oauth2/api/partner/webhooksPartnerList your webhooks200
POST/oauth2/api/partner/webhooksPartnerAdd a webhook200
PUT/oauth2/api/partner/webhooks/{webhookId}PartnerReplace a webhook200
DELETE/oauth2/api/partner/webhooks/{webhookId}PartnerRemove a webhook204
GET/oauth2/api/partner/webhook-configPartnerSee how Sporty's authenticates to your webhook URL200
PUT/oauth2/api/partner/webhook-configPartnerSet how Sporty's authenticates to your webhook URL200
GET/oauth2/api/meStudentThe connected student's profile200
GET/oauth2/api/courses/{webAppId}/progressStudentThe student's progress in one course200

Go-live checklist

Support

Your Sporty's partner contact can issue or replace credentials, add courses to your catalog, register redirect URLs, link your schools on Sporty's, and send test webhooks.

When you report a problem, include the time (UTC), the endpoint, the HTTP status and response body, and the purchaseId or partnerFlightSchoolId involved. Never send your client secret or a student's tokens.