# Group challenges

> Follow your students through an organization's group challenges, the groups their teachers form, each step and its files, and what happened in a group.

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

| Operation | Request | Permission |
|---|---|---|
| [List the challenges](https://hub.main-team.org/api/reference/list-group-challenges) | `GET /v1/<organizationId>/group-challenge` | `group-challenge/read` |
| [Fetch one challenge](https://hub.main-team.org/api/reference/get-group-challenge) | `GET /v1/<organizationId>/group-challenge/<challengeId>` | `group-challenge/read` |
| [List your students in a challenge](https://hub.main-team.org/api/reference/list-group-challenge-students) | `GET /v1/<organizationId>/group-challenge/<challengeId>/student` | `group-challenge/read` |
| [Fetch one of your students in a challenge](https://hub.main-team.org/api/reference/get-group-challenge-student) | `GET /v1/<organizationId>/group-challenge/<challengeId>/student/<studentId>` | `group-challenge/read` |
| [List your students' groups](https://hub.main-team.org/api/reference/list-group-challenge-groups) | `GET /v1/<organizationId>/group-challenge/<challengeId>/group` | `group-challenge/read` |
| [Fetch one group](https://hub.main-team.org/api/reference/get-group-challenge-group) | `GET /v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>` | `group-challenge/read` |
| [List what happened in a group](https://hub.main-team.org/api/reference/list-group-challenge-activity) | `GET /v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/activity` | `group-challenge/read` |
| [Submit a step for your student](https://hub.main-team.org/api/reference/submit-group-challenge-step) | `POST /v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/step/<stepId>/submit` | `group-challenge/submit` |
| [Send a group's work for your student](https://hub.main-team.org/api/reference/submit-group-challenge-work) | `POST /v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/final-submit` | `group-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](https://hub.main-team.org/api/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

```bash
curl -s "https://api.main-team.org/v1/<organizationId>/group-challenge" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "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:

```bash
curl -s "https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/student" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "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`](https://hub.main-team.org/api/reference/list-org-students) lists: yours, activated for the organization, and signed in to it at least once. `getGroupChallengeStudent` answers the same for one student, activated or not.

| `state` | Meaning | What moves it on |
|---|---|---|
| `not_eligible` | Their grade is in none of the challenge's grade groups | Nothing, unless their grade was wrong: correct it with `updateOrgStudent` |
| `not_in_group` | Eligible, and in no group yet | Their teacher adds them to a group in the panel |
| `group_pending` | In a group the teacher is still preparing | The teacher confirms the group |
| `group_active` | In a confirmed group, working through the steps | The group submits every step, then sends its work |
| `group_completed` | Their group has sent its work | Nothing: 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`](https://hub.main-team.org/api/reference/link-student-supervisor).

**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`](https://hub.main-team.org/api/reference/create-signin-link), and the link opens that page, signed in; `{userId}` is filled in for you. See [Sending a student to the panel](https://hub.main-team.org/api/tutorials/send-student-to-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:

```bash
curl -s "https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "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 `status` | Meaning |
|---|---|
| `awaiting_payment`, `draft` | The teacher is preparing the group. It has no steps yet. |
| `finalized` | The teacher has confirmed it. Its first step is open, and each submitted step opens the next. |
| `completed` | Its 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:

```json
{ "role": "student", "studentId": null, "name": null, "yours": false }
```

| `role` | Who |
|---|---|
| `student` | A member of the group |
| `teacher` | The group's teacher |
| `administrator` | The organizers |
| `partner` | An API account acting for a student; `yours: true` when it is yours |
| `system` | The 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.

```bash
curl -s "https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/activity?limit=50" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{
  "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`).

```bash
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>" }'
```

```json
{
  "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:

```json
{
  "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

```bash
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>" }'
```

```json
{
  "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

| Situation | Answer |
|---|---|
| Group challenges are not switched on for the organization | `404 not_found`, `Cannot GET /v1/<organizationId>/group-challenge…` |
| The challenge does not exist, or is not published | `404 not_found`, `Group challenge not found!` |
| The student is not yours, or does not exist | `404 not_found`, `Student not found!` |
| The student has never signed in to this organization | `409 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 exist | `404 not_found`, `Group not found!` |
| An id in the path is not 24 hexadecimal characters | `400 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 else | `400 bad_request` |
| A submit the group's state does not allow | `409 conflict`, with `details.reason` (see [Submitting for your student](#submitting-for-your-student)) |
| A step the group does not have | `404 not_found`, `Step not found!` |
| Someone else is changing the group at that moment | `503 service_unavailable`, `Retry-After: 1`, `details.reason: busy` |
