# Send a group’s finished work for one of your students

- Endpoint: `POST /v1/{organizationId}/group-challenge/{challengeId}/group/{groupId}/final-submit`
- Production: `https://api.main-team.org/v1/{organizationId}/group-challenge/{challengeId}/group/{groupId}/final-submit`
- Sandbox: `https://apisnd.main-team.org/v1/{organizationId}/group-challenge/{challengeId}/group/{groupId}/final-submit`
- Operation: `submitGroupChallengeWork` (Group challenges)
- Authentication: `Authorization: Bearer <token>`, a short-lived token you sign with your API key and secret
- Permission: `group-challenge/submit:$org:$ID`

## Description

Sends a group’s finished work, once every step is submitted, for one of your students who is a member of the group, exactly as the student would in the panel. The group becomes `completed`. Read the group first: `canFinalSubmit` says whether this would be accepted now.

The body names the student you act for: `{ "studentId": "<studentId>" }`. The group’s history records the submit as your account (`partner`) acting for them.

**Checks, in order.** The first that fails decides the answer, and nothing is written for any of them.

1. The student is yours (`404`), and has signed in to this organization once (`409`).
2. The challenge exists and is published or closed (`404`).
3. The group belongs to the challenge and the student is an active member of it (`404`).
4. The work has not been sent already. If it has, the answer is `200` with `changed: false` and "Group work already submitted.", and nothing changes, so a retry after a timeout is safe.
5. The challenge is published (`409`, `challenge_closed`) and now is between `windowStart` and `windowEnd` (`409`, `window_closed`).
6. The teacher has confirmed the group (`409`, `payment_pending` or `group_not_confirmed`).
7. Every step is submitted (`409`, `steps_incomplete`, with `stepsSubmitted` and `stepCount` in `details`).

Every `409` carries `error.details.reason`; branch on it, not on the message.

**Side effects.** The group becomes `completed`, and an entry is added to its history. Every member of the group and its teacher receive an e-mail saying the group has sent its work.

**Busy.** As for `submitGroupChallengeStep`: `503` with `Retry-After: 1` and `details.reason: busy` when someone is changing the group at that moment; nothing was written, so send it again.

## Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `organizationId` | path | yes | string | The organization’s `_id`: 24 hexadecimal digits, as `listOrganizations` (`GET /v1/organization`) lists it. |
| `challengeId` | path | yes | string | The group challenge’s `_id`, from `listGroupChallenges`. |
| `groupId` | path | yes | string | The group’s `_id`, from `listGroupChallengeGroups` or a student’s `group`. |

## Request body

Required fields: `studentId`.

```json
{
  "studentId": "6650a1b2c3d4e5f6a7b8c9d0"
}
```

## Response

`200` Success: `message` is "Group work submitted.". Or `message` is "Group work already submitted.": The work had already been sent, by anyone. `changed` is `false` and nothing changed.

```json
{
  "data": {
    "changed": true,
    "finalSubmittedAt": "2026-10-20T16:02:11.000Z",
    "groupId": "6650a1b2c3d4e5f6a7b8c9ed",
    "groupStatus": "completed"
  },
  "message": "Group work submitted.",
  "success": true
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | The body is not valid JSON, breaks a field’s rules, or has a field this operation does not accept ("property <name> should not exist"). |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | `challengeId` is not 24 hexadecimal digits. |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | `groupId` is not 24 hexadecimal digits. |
| 401 | [`unauthorized`](https://hub.main-team.org/api/errors#unauthorized) | The token is missing or malformed, is not signed with your account’s `apiSecret`, breaks the `iat` and `exp` rules, has expired or been revoked, or its account is not active. All of these answer the same. |
| 403 | [`forbidden`](https://hub.main-team.org/api/errors#forbidden) | The token is valid, but no role on your account allows `group-challenge/submit` on the organization in the path, or a role denies it. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | `organizationId` is not the `_id` of an organization. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | Group challenges are not switched on for this organization. The answer is the one an unknown path gets; nothing was read. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | No student of yours has this id: it matches nobody, or another account registered the student. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | No group challenge of this organization has this id, or it is not published: a draft, archived or deleted challenge answers the same. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | No group of this challenge has this id with the student as an active member: a missing group, a deleted one and one the student is not in get the same answer. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The student has never signed in to this organization, so it holds no record of them yet. Send them a sign-in link (`createSigninLink`), and try again once they have used it. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The organizers have closed the challenge (`status: closed`). |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | Now is before `windowStart` or after `windowEnd`. `details` carries both. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The teacher is still preparing the group (`status: awaiting_payment`). |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | The teacher has not confirmed the group yet (`status: draft`), so it has no steps. |
| 409 | [`conflict`](https://hub.main-team.org/api/errors#conflict) | A step is not submitted yet. `details` counts them. |
| 413 | [`payload_too_large`](https://hub.main-team.org/api/errors#payload_too_large) | The body is larger than 100 kB. |
| 415 | [`unsupported_media_type`](https://hub.main-team.org/api/errors#unsupported_media_type) | The body declares a charset that is not a UTF one (send UTF-8), or a `Content-Encoding` other than gzip, deflate or br. |
| 429 | [`too_many_requests`](https://hub.main-team.org/api/errors#too_many_requests) | Your account has made more than 100 requests to this operation in the current 60-second window. Wait the seconds in `Retry-After` before sending again. |
| 500 | [`internal_error`](https://hub.main-team.org/api/errors#internal_error) | Something failed on our side. Retry later, and quote `request_id` if it goes on. |
| 503 | [`service_unavailable`](https://hub.main-team.org/api/errors#service_unavailable) | Someone else — a member in the panel or the app, or another request of yours — is changing the group at this moment. Nothing was written. Wait the second in `Retry-After` and send the same request again. |

## Code samples

### curl

```bash
# $TOKEN: a short-lived token you minted with your API key and secret
curl -sS -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' \
  --data-binary @- <<'JSON'
{
  "studentId": "6650a1b2c3d4e5f6a7b8c9d0"
}
JSON
```

### Node.js

```js
const token = process.env.TOKEN; // a short-lived token you minted with your API key and secret

const res = await fetch('https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/final-submit', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "studentId": "6650a1b2c3d4e5f6a7b8c9d0"
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
console.log(body.data);
```

### PHP

```php
<?php
$token = getenv('TOKEN'); // a short-lived token you minted with your API key and secret

$ch = curl_init('https://api.main-team.org/v1/<organizationId>/group-challenge/<challengeId>/group/<groupId>/final-submit');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'studentId' => '6650a1b2c3d4e5f6a7b8c9d0',
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
if ($response === false) {
    throw new RuntimeException(curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$body = json_decode($response, true);
if ($status >= 400) {
    $error = $body['error'];
    throw new RuntimeException("$status {$error['code']}: {$error['message']}");
}
print_r($body['data']);
```
