# Download a result report file

- Endpoint: `GET /v1/{organizationId}/report/download/{reportId}`
- Production: `https://api.main-team.org/v1/{organizationId}/report/download/{reportId}`
- Sandbox: `https://apisnd.main-team.org/v1/{organizationId}/report/download/{reportId}`
- Operation: `downloadReport` (Documents)
- Authentication: `Authorization: Bearer <token>`, a short-lived token you sign with your API key and secret
- Permission: `report/read:$org:$ID`

## Description

Sends the file of a released result report of one of your students: the bytes themselves, not JSON. `Content-Disposition` carries its name.

Every refusal of the report itself is the same `404` "Not found!": no report has this id or `shortId`, it is not released or was withdrawn, it belongs to another account’s student, or it has no file. So a download never shows whether a report exists in someone else’s hands.

Errors always arrive as JSON before the first byte. A transfer that ends early, or with fewer bytes than `Content-Length`, has failed: discard what you received.

## Parameters

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `organizationId` | path | yes | string | The organization’s `_id`: 24 hexadecimal digits, as `listOrganizations` (`GET /v1/organization`) lists it. |
| `reportId` | path | yes | string | The report’s `_id` (24 hexadecimal digits) or its 10-character `shortId`, both from `listStudentReports`. A `shortId` is matched exactly, case included. |

## Response

`200` The report file, streamed. Content-Type is whatever the object was stored with. Content-Disposition carries an ASCII `filename` and the real name as RFC 5987 `filename*=UTF-8''…`; prefer the latter.

A file (application/octet-stream, application/pdf), not JSON.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 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 `report/read` 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) | No report has this id or shortId, it is not released, it belongs to another account's student, it has no file attached, or its file is missing from storage. Every one of these answers the same, so the status says nothing about another account's ids. |
| 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. |
| 500 | [`internal_error`](https://hub.main-team.org/api/errors#internal_error) | File storage is not configured or could not be reached. |

## Code samples

### curl

```bash
# $TOKEN: a short-lived token you minted with your API key and secret
# -f: an error status exits non-zero instead of being saved as report.pdf
curl -sS -f -o report.pdf 'https://api.main-team.org/v1/<organizationId>/report/download/<reportId>' \
  -H "Authorization: Bearer $TOKEN"
```

### Node.js

```js
import { writeFile } from 'node:fs/promises';

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>/report/download/<reportId>', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
await writeFile('report.pdf', Buffer.from(await res.arrayBuffer()));
```

### 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>/report/download/<reportId>');
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);
if ($status >= 400) {
    $error = json_decode($response, true)['error'];
    throw new RuntimeException("$status {$error['code']}: {$error['message']}");
}
file_put_contents('report.pdf', $response);
```
