Skip to content
API documentation
View as MarkdownOpen in Claude

Guides

Group challenges

A group challenge is a project your students do together, in small groups, between two dates. Their teacher forms each group in the panel from their own students, and the group then works through the challenge's steps in order: its members upload their work for a step in the panel or the app, submit the step, and the next one opens. When every step is submitted, the group sends its finished work.

This API lets you follow that for your own students: which challenges an organization runs, which of your students can take part, the groups they are in, each step with its files, and everything that happened in a group. It can also submit a step, or send the finished work, for one of your students, as they would themselves. It never creates a group, adds a student to one or uploads a file: those stay with the teacher and the students.

Operations

OperationRequestPermission
List the challengesGET /v1/<organizationId>/group-challengegroup-challenge/read
Fetch one challengeGET /v1/<organizationId>/group-challenge/<challengeId>group-challenge/read
List your students in a challengeGET /v1/<organizationId>/group-challenge/<challengeId>/studentgroup-challenge/read
Fetch one of your students in a challengeGET /v1/<organizationId>/group-challenge/<challengeId>/student/<studentId>group-challenge/read
List your students' groupsGET /v1/<organizationId>/group-challenge/<challengeId>/groupgroup-challenge/read
Fetch one groupGET /v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>group-challenge/read
List what happened in a groupGET /v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/activitygroup-challenge/read
Submit a step for your studentPOST /v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/step/<stepId>/submitgroup-challenge/submit
Send a group's work for your studentPOST /v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/final-submitgroup-challenge/submit

Every permission's target is the organization in the path, or *. */read, */* and * grant the reads too, and */* and * grant the submits; see Permissions.

Where group challenges are available

Group challenges are switched on per organization. On an organization where they are not, every one of these operations answers 404 not_found with the message an unknown path gets, Cannot GET /v1/<organizationId>/group-challenge, and nothing is read. Ask support which organizations run them.

The challenges

curl -s "https://api.main-team.org/v1/<organizationId>/group-challenge" \
  -H "Authorization: Bearer $TOKEN"
{
  "success": true,
  "message": "Group challenges fetched successfully.",
  "data": [
    {
      "_id": "6650a1b2c3d4e5f6a7b8c9eb",
      "name": "STEM Maker Challenge",
      "status": "published",
      "isOpen": true,
      "windowStart": "2026-10-01T00:00:00.000Z",
      "windowEnd": "2026-12-21T23:59:59.999Z",
      "minStudents": 2,
      "maxStudents": 3,
      "isPaid": false,
      "gradeGroups": [
        {
          "_id": "6650a1b2c3d4e5f6a7b8c9ec",
          "label": "Group 7-8-9",
          "grades": [
            { "_id": "6650a1b2c3d4e5f6a7b8c9d4", "name": "7" },
            { "_id": "6650a1b2c3d4e5f6a7b8c9d5", "name": "8" },
            { "_id": "6650a1b2c3d4e5f6a7b8c9d6", "name": "9" }
          ]
        }
      ],
      "steps": [
        { "_id": "6650a1b2c3d4e5f6a7b8c9ee", "order": 1, "title": "Project proposal", "fileTypes": ["pdf", "docx"], "maxFileBytes": 20971520, "maxFiles": 1 },
        { "_id": "6650a1b2c3d4e5f6a7b8c9f1", "order": 2, "title": "Project documentation", "fileTypes": ["pdf", "docx"], "maxFileBytes": 20971520, "maxFiles": 1 }
      ]
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}
  • A challenge is listed while it is published, and after the organizers close it (closed). One they have not published yet is not listed, and asked for by id it answers 404 not_found, Group challenge not found!.
  • isOpen is true while the challenge takes work: published, and now between windowStart and windowEnd (UTC, both inclusive). Nothing is submitted outside those dates.
  • Grade groups. Every member of a group comes from the same grade group. A student whose grade is in none of them cannot take part.
  • minStudents and maxStudents bound the size of a group.

Your students

listGroupChallengeStudents answers, for each of your students in this organization, where they stand:

curl -s "https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/student" \
  -H "Authorization: Bearer $TOKEN"
{
  "success": true,
  "message": "Students fetched successfully.",
  "data": [
    {
      "studentId": "6650a1b2c3d4e5f6a7b8c9d0",
      "firstName": "Ada",
      "lastName": "Lovelace",
      "grade": { "_id": "6650a1b2c3d4e5f6a7b8c9d5", "name": "8" },
      "eligible": true,
      "gradeGroup": { "_id": "6650a1b2c3d4e5f6a7b8c9ec", "label": "Group 7-8-9" },
      "teacherLinked": true,
      "state": "group_active",
      "group": { "_id": "6650a1b2c3d4e5f6a7b8c9ed", "displayName": "Group 7KQ2MX", "status": "finalized" },
      "panelPath": "/stadia/{userId}/group-challenges/6650a1b2c3d4e5f6a7b8c9eb"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}

It lists the students listOrgStudents lists: yours, activated for the organization, and signed in to it at least once. getGroupChallengeStudent answers the same for one student, activated or not.

stateMeaningWhat moves it on
not_eligibleTheir grade is in none of the challenge's grade groupsNothing, unless their grade was wrong: correct it with updateOrgStudent
not_in_groupEligible, and in no group yetTheir teacher adds them to a group in the panel
group_pendingIn a group the teacher is still preparingThe teacher confirms the group
group_activeIn a confirmed group, working through the stepsThe group submits every step, then sends its work
group_completedTheir group has sent its workNothing: they are done

A group wins over the grade: a student who is in a group shows it even if their grade changed since. New values may be added; treat one you do not know as "nothing to do yet".

Only a teacher puts students in groups. teacherLinked says whether a teacher is linked to the student in this organization. A student in not_in_group with teacherLinked: false needs one first: link it with linkStudentSupervisor.

Sending a student to the challenge. panelPath is where the challenge's page is on the organization's panel. Send it as redirect to createSigninLink, and the link opens that page, signed in; {userId} is filled in for you. See Sending a student to the panel.

Groups

listGroupChallengeGroups lists the groups of a challenge that at least one of your students is an active member of, oldest first. getGroupChallengeGroup reads one, with its steps and their files:

curl -s "https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>" \
  -H "Authorization: Bearer $TOKEN"
{
  "success": true,
  "message": "Group fetched successfully.",
  "data": {
    "_id": "6650a1b2c3d4e5f6a7b8c9ed",
    "challengeId": "6650a1b2c3d4e5f6a7b8c9eb",
    "shortCode": "7KQ2MX",
    "name": null,
    "displayName": "Group 7KQ2MX",
    "status": "finalized",
    "gradeGroup": { "_id": "6650a1b2c3d4e5f6a7b8c9ec", "label": "Group 7-8-9" },
    "memberCount": 3,
    "yourStudents": [
      { "studentId": "6650a1b2c3d4e5f6a7b8c9d0", "firstName": "Ada", "lastName": "Lovelace", "gradeName": "8", "addedAt": "2026-10-05T13:20:00.000Z" }
    ],
    "stepCount": 2,
    "stepsSubmitted": 1,
    "createdAt": "2026-10-05T13:20:00.000Z",
    "finalizedAt": "2026-10-12T08:15:00.000Z",
    "finalSubmittedAt": null,
    "finalSubmittedBy": null,
    "canFinalSubmit": false,
    "steps": [
      {
        "_id": "6650a1b2c3d4e5f6a7b8c9ee",
        "order": 1,
        "title": "Project proposal",
        "state": "submitted",
        "openedAt": "2026-10-12T08:15:00.000Z",
        "submittedAt": "2026-10-20T16:02:11.000Z",
        "submittedBy": { "role": "student", "studentId": null, "name": null, "yours": false },
        "reopenCount": 0,
        "canSubmit": false,
        "files": [
          {
            "_id": "6650a1b2c3d4e5f6a7b8c9ef",
            "status": "submitted",
            "name": null,
            "fileType": "pdf",
            "size": 1048576,
            "uploadedBy": { "role": "student", "studentId": null, "name": null, "yours": false },
            "completedAt": "2026-10-20T15:47:03.000Z",
            "submittedAt": "2026-10-20T16:02:11.000Z"
          }
        ]
      },
      {
        "_id": "6650a1b2c3d4e5f6a7b8c9f1",
        "order": 2,
        "title": "Project documentation",
        "state": "open",
        "openedAt": "2026-10-20T16:02:11.000Z",
        "submittedAt": null,
        "submittedBy": null,
        "reopenCount": 0,
        "canSubmit": false,
        "files": []
      }
    ]
  }
}
Group statusMeaning
awaiting_payment, draftThe teacher is preparing the group. It has no steps yet.
finalizedThe teacher has confirmed it. Its first step is open, and each submitted step opens the next.
completedIts work has been sent.
  • Steps appear once the group is finalized. A step is locked until the one before it is submitted, open while the group works on it, and submitted once sent. The organizers may reopen a step; reopenCount counts that.
  • Files. A step's files are uploading, draft (uploaded, not yet submitted) or submitted. Members upload in the panel or the app, never through this API.
  • canSubmit on a step and canFinalSubmit on the group say whether a submit would be accepted right now.
  • A group none of your students is an active member of answers 404 not_found, Group not found!, exactly like a group that does not exist. A deleted group does too.

Your students, and everybody else

A group can mix your students with students from other schools. You see your own students by name, with the studentId you registered them with. Everyone else — the other members, the teacher, the organizers, another partner — is shown by role alone:

{ "role": "student", "studentId": null, "name": null, "yours": false }
roleWho
studentA member of the group
teacherThe group's teacher
administratorThe organizers
partnerAn API account acting for a student; yours: true when it is yours
systemThe platform itself

A file's name is shown only when one of your students uploaded it. yourStudents lists your students in a group; the others are counted in memberCount.

What happened in a group

listGroupChallengeActivity lists a group's history, newest first: the group created, members added, the group confirmed, files uploaded, steps submitted, the work sent.

curl -s "https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/activity?limit=50" \
  -H "Authorization: Bearer $TOKEN"
{
  "success": true,
  "message": "Activity fetched successfully.",
  "data": [
    {
      "_id": "6650a1b2c3d4e5f6a7b8c9f0",
      "at": "2026-10-20T16:02:11.000Z",
      "event": "step_submitted",
      "actor": { "role": "student", "studentId": "6650a1b2c3d4e5f6a7b8c9d0", "name": "Ada Lovelace", "yours": true },
      "via": "app",
      "onBehalfOf": null,
      "subject": null,
      "step": { "_id": "6650a1b2c3d4e5f6a7b8c9ee", "order": 1 },
      "file": null
    }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 9, "totalPages": 1 }
}

It is the history every member of the group can read. Notes and details the organizers keep for themselves are not included. event and via may grow new values; ignore one you do not know.

Submitting for your student

A step is submitted, and the finished work sent, by a member of the group, in the panel or the app. You can do either for one of your students who is an active member of the group: the body names them, and the group's history records your account (partner, yours: true) acting for them (onBehalfOf).

curl -s -X POST "https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/step/<stepId>/submit" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "studentId": "<studentId>" }'
{
  "success": true,
  "message": "Step submitted.",
  "data": {
    "groupId": "6650a1b2c3d4e5f6a7b8c9ed",
    "groupStatus": "finalized",
    "step": { "_id": "6650a1b2c3d4e5f6a7b8c9ee", "order": 1, "state": "submitted", "submittedAt": "2026-10-20T16:02:11.000Z" },
    "nextStep": { "_id": "6650a1b2c3d4e5f6a7b8c9f1", "order": 2, "state": "open" },
    "changed": true
  }
}

A submit does what the student's own would: the step's uploaded files are submitted with it, an upload still running for it is stopped, and the next step opens. Uploading is not something this API does: when a step has no file, a member uploads one first.

Read before you write. getGroupChallengeGroup says, for each step, whether a submit would be accepted now (canSubmit), and for the group whether the work can be sent (canFinalSubmit).

Checks, in order. Nothing is written for any refusal:

  1. The student is yours (404 not_found, Student not found!) and has signed in to the organization once (409 conflict).
  2. The challenge exists and is published or closed (404, Group challenge not found!).
  3. The group belongs to the challenge and the student is an active member of it (404, Group not found!).
  4. Already done? A step already submitted, or work already sent, answers 200 with changed: false (Step already submitted., Group work already submitted.), and nothing changes.
  5. The challenge takes work: 409 with details.reason challenge_closed, or window_closed outside windowStart–windowEnd.
  6. The teacher has confirmed the group: 409 payment_pending or group_not_confirmed.
  7. For a step: the group has it (404, Step not found!), it is open (409 step_locked), and a file has been uploaded for it (409 step_empty). For the work: every step is submitted (409 steps_incomplete, counting stepsSubmitted and stepCount).

Branch on error.details.reason, not on the message:

{
  "error": {
    "code": "conflict",
    "message": "Every step has to be submitted before the group’s work can be sent.",
    "documentation_url": "https://hub.main-team.org/api/errors#conflict",
    "request_id": "3c2b1a0f-9e8d-4c7b-a6f5-e4d3c2b1a0f9",
    "details": { "reason": "steps_incomplete", "stepsSubmitted": 2, "stepCount": 3 }
  }
}

step_empty and steps_incomplete wait on the members' work: don't retry them in a loop.

Safe to retry. Because a repeat answers 200 with changed: false, you can send a submit again after a timeout or a dropped connection and read changed to learn whether this request or an earlier one did it.

Busy. A group is changed by one request at a time, whether it comes from the panel, the app or this API. When someone is changing the group at that moment, the answer is 503 service_unavailable with Retry-After: 1 and details.reason: busy, and nothing was written. Wait a second and send the same request again.

Sending the work

curl -s -X POST "https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/final-submit" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "studentId": "<studentId>" }'
{
  "success": true,
  "message": "Group work submitted.",
  "data": {
    "groupId": "6650a1b2c3d4e5f6a7b8c9ed",
    "groupStatus": "completed",
    "finalSubmittedAt": "2026-11-02T10:00:00.000Z",
    "changed": true
  }
}

Once every step is submitted, sending the work completes the group. Every member of the group and its teacher receive an e-mail saying the group has sent its work, as when a member sends it themselves.

Refusals

SituationAnswer
Group challenges are not switched on for the organization404 not_found, Cannot GET /v1/<organizationId>/group-challenge…
The challenge does not exist, or is not published404 not_found, Group challenge not found!
The student is not yours, or does not exist404 not_found, Student not found!
The student has never signed in to this organization409 conflict, Student has never signed in to stem, so stem holds no record for them. …
None of your students is an active member of the group, or it does not exist404 not_found, Group not found!
An id in the path is not 24 hexadecimal characters400 bad_request, Invalid value for 'challengeId': expected ObjectId. (naming the id)
studentId in a submit's body is not an id, or the body carries anything else400 bad_request
A submit the group's state does not allow409 conflict, with details.reason (see Submitting for your student)
A step the group does not have404 not_found, Step not found!
Someone else is changing the group at that moment503 service_unavailable, Retry-After: 1, details.reason: busy

Search the API documentation

Guides, endpoints by name, path or permission, and error codes such as not_found.