# Follow a batch of students you sent

- Endpoint: `GET /v1/student/import/{importId}`
- Production: `https://api.main-team.org/v1/student/import/{importId}`
- Sandbox: `https://apisnd.main-team.org/v1/student/import/{importId}`
- Operation: `getStudentImport` (Students)
- Authentication: `Authorization: Bearer <token>`, a short-lived token you sign with your API key and secret
- Permission: `student/create:$org:$ID`

## Description

Returns one of your imports and one page of its rows, in the order you sent them.

`status` is `queued` until a server picks the batch up, `running` while it registers, then `succeeded` or `failed`. Poll every few seconds; a batch of a thousand takes seconds, not minutes, once it starts.

**On `succeeded`** every row is `registered` and carries the student’s `_id`: keep them, and use them with `getStudent`, `createSigninLink` and `createApplication`. **On anything else** no student of this batch was registered, every row is `skipped`, and `failure` says why; the rows that explain it carry an `error`.

An import stops being readable 30 days after it finishes, and then answers `404` exactly like one that never existed. The students stay registered. An import another account sent answers the same way.

## Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `importId` | path | yes | string | The import’s `_id`, as `createStudentImport` returned it. |
| `page` | query | no | number | Page number. Defaults to 1. |
| `limit` | query | no | number | Items per page. Defaults to 20, max 100. |

## Response

`200` Success: `message` is "Import fetched successfully.".

```json
{
  "success": true,
  "message": "Import fetched successfully.",
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 57,
    "totalPages": 3
  },
  "data": {
    "_id": "65f0c2a1d3e4f5a6b7c8d901",
    "status": "queued",
    "total": 250,
    "registered": 250,
    "clientReference": "year-10-autumn-2026",
    "failure": {
      "code": "rows_rejected",
      "message": "Two rows could no longer be registered. Nothing was registered."
    },
    "students": [
      {
        "row": 0,
        "email": "jane.doe@example.com",
        "externalRef": "roster-2026-114",
        "status": "registered",
        "studentId": "6650a1b2c3d4e5f6a7b8c9d0",
        "error": {
          "code": "email_taken_by_your_student",
          "message": "One of your students already has this email address.",
          "studentId": "6650a1b2c3d4e5f6a7b8c9d0",
          "duplicateOf": 12
        }
      }
    ],
    "createdAt": "2026-10-06T08:15:00.000Z",
    "startedAt": "2026-10-06T08:15:04.000Z",
    "finishedAt": "2026-10-06T08:15:09.000Z",
    "expiresAt": "2026-11-05T08:15:09.000Z"
  }
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | [`bad_request`](https://hub.main-team.org/api/errors#bad_request) | `importId` 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 `student/create` on `mto`, the organization every operation without `:organizationId` acts on, or a role denies it. |
| 404 | [`not_found`](https://hub.main-team.org/api/errors#not_found) | No import of yours has this `importId`, it has expired, or bulk registration is not switched on for the environment you are calling. An import another account sent answers the same. |
| 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. |

## Code samples

### curl

```bash
# $TOKEN: a short-lived token you minted with your API key and secret
curl -sS 'https://api.main-team.org/v1/student/import/<importId>?page=1&limit=20' \
  -H "Authorization: Bearer $TOKEN"
```

### 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/student/import/<importId>?page=1&limit=20', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
console.log(body.data, body.pagination);
```

### 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/student/import/<importId>?page=1&limit=20');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $token,
    ],
    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']);
print_r($body['pagination']);
```
