# Download a certificate file

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

## Description

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

Every refusal of the certificate itself is the same `404` "Not found!": no certificate has this id or `shortId`, it is not released, it belongs to another account’s student, or it has no file. So a download never shows whether a certificate 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. |
| `certificateId` | path | yes | string | The certificate’s `_id` (24 hexadecimal digits) or its 10-character `shortId`, both from `listStudentCertificates`. A `shortId` is matched exactly, case included. |

## Response

`200` The certificate 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 `certificate/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 certificate 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 certificate.pdf
curl -sS -f -o certificate.pdf 'https://api.main-team.org/v1/<organizationId>/certificate/download/<certificateId>' \
  -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>/certificate/download/<certificateId>', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
await writeFile('certificate.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>/certificate/download/<certificateId>');
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('certificate.pdf', $response);
```
