> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enemhub.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Exemplos

> Exames, filtros, paginação e o que fazer quando a API responde 429.

Todos os exemplos assumem a chave em `ENEMHUB_API_KEY` e a base
`https://api.enemhub.com.br`.

## Listar os exames

É por aqui que se começa: `/exams` devolve o catálogo inteiro, sem paginação, e
é dele que saem os `examId` usados como filtro.

```bash theme={null}
curl https://api.enemhub.com.br/v1/vestibulares/exams \
  -H "X-API-Key: $ENEMHUB_API_KEY"
```

```json theme={null}
{
  "data": [
    { "id": "3b1e...", "name": "FUVEST 2023", "institution": "USP", "type": "vestibular" },
    { "id": "7c9a...", "name": "ENEM 2023", "institution": "INEP", "type": "enem" }
  ]
}
```

<Note>
  `institution` e `type` podem vir `null` para exames sem essa classificação.
  Não assuma que estão preenchidos.
</Note>

## Filtrar questões

`GET /v1/vestibulares/questions` aceita cinco filtros, todos opcionais e
combináveis:

| Parâmetro    | Tipo                         | Observação                                                    |
| ------------ | ---------------------------- | ------------------------------------------------------------- |
| `examId`     | uuid                         | ID do exame, vindo de `/exams`.                               |
| `year`       | inteiro                      | Ano do exame, ex. `2023`.                                     |
| `subjectId`  | uuid                         | ID da matéria. Vem no campo `subject.id` de qualquer questão. |
| `difficulty` | `easy` \| `medium` \| `hard` | Nível de dificuldade.                                         |
| `page`       | inteiro                      | Começa em `1`. Padrão `1`.                                    |
| `limit`      | inteiro                      | De `1` a `100`. Padrão `20`.                                  |

```bash Biológicas difíceis da FUVEST theme={null}
curl -G https://api.enemhub.com.br/v1/vestibulares/questions \
  -H "X-API-Key: $ENEMHUB_API_KEY" \
  -d examId=3b1e... \
  -d difficulty=hard \
  -d limit=50
```

Sem `examId`, a listagem cobre **todos** os exames do produto, ENEM incluído.

## Paginar até o fim

`meta.total` é a contagem do conjunto filtrado inteiro, então dá para calcular o
número de páginas antes de começar:

```js theme={null}
const BASE = "https://api.enemhub.com.br/v1/vestibulares/questions";
const headers = { "X-API-Key": process.env.ENEMHUB_API_KEY };

async function todasAsQuestoes(filtros = {}) {
  const limit = 100; // o máximo aceito — menos páginas, menos requisições
  const todas = [];

  for (let page = 1; ; page++) {
    const query = new URLSearchParams({ ...filtros, page, limit });
    const res = await fetch(`${BASE}?${query}`, { headers });

    if (!res.ok) throw new Error(`${res.status} ao buscar a página ${page}`);

    const { data, meta } = await res.json();
    todas.push(...data);

    if (todas.length >= meta.total || data.length === 0) return todas;
  }
}

const fuvest = await todasAsQuestoes({ examId: "3b1e..." });
```

<Tip>
  `limit=100` é o teto. Espelhar um catálogo inteiro a 20 por vez custa cinco
  vezes mais requisições da sua cota do que a 100 por vez.
</Tip>

## Espelhar o catálogo por exame

O caso mais comum de carga inicial: varrer exame a exame, para poder retomar de
onde parou se algo falhar no meio.

```js theme={null}
const exames = await fetch("https://api.enemhub.com.br/v1/vestibulares/exams", { headers })
  .then((r) => r.json())
  .then((r) => r.data);

for (const exame of exames) {
  const questoes = await todasAsQuestoes({ examId: exame.id });
  await salvar(exame, questoes);
  console.log(`${exame.name}: ${questoes.length} questões`);
}
```

## Buscar uma questão específica

Diferente da listagem, esta rota devolve a questão **sem envelope**:

```bash theme={null}
curl https://api.enemhub.com.br/v1/vestibulares/questions/9f3c1a2e-... \
  -H "X-API-Key: $ENEMHUB_API_KEY"
```

Um ID que não existe — ou que pertence a um exame fora deste produto — devolve
`404`:

```json theme={null}
{ "error": "Questão não encontrada" }
```

## Montar uma prova

Correta e alternativas vêm juntas na resposta, então embaralhar e esconder o
gabarito é trabalho do seu lado:

```js theme={null}
function paraOAluno(questao) {
  return {
    id: questao.id,
    enunciado: questao.statement,
    imagem: questao.imageUrl,
    exame: questao.exam.name,
    // `isCorrect` e `correctAlternative` ficam de fora — o gabarito não desce
    // para o cliente.
    alternativas: questao.alternatives.map(({ id, letter, text, imageUrl }) => ({
      id, letra: letter, texto: text, imagem: imageUrl,
    })),
  };
}

function corrigir(questao, alternativaId) {
  return questao.alternatives.find((a) => a.id === alternativaId)?.isCorrect ?? false;
}
```

## Lidar com limites

Toda resposta de `/v1/*` traz dois headers:

| Header                  | Significado                                       |
| ----------------------- | ------------------------------------------------- |
| `X-RateLimit-Limit`     | Requisições por minuto permitidas pelo seu plano. |
| `X-RateLimit-Remaining` | Quantas ainda cabem no minuto corrente.           |

Ao estourar, a resposta é `429` com `Retry-After: 60`. São três limites
independentes:

* **Por minuto** — definido pelo plano.
* **Por dia** — 10.000 requisições, igual para todos os planos.
* **Por mês** — a cota do plano. Ao estourar, a mensagem pede upgrade.

<Tip>
  Uma requisição recusada com `429` **não** consome cota mensal. Estourar o
  limite por minuto não queima o seu volume contratado.
</Tip>

```js Retry com backoff theme={null}
async function comRetry(url, init, tentativas = 3) {
  for (let i = 0; i < tentativas; i++) {
    const res = await fetch(url, init);
    if (res.status !== 429) return res;

    const espera = Number(res.headers.get("Retry-After") ?? 60);
    await new Promise((r) => setTimeout(r, espera * 1000));
  }
  throw new Error("Rate limit persistente após 3 tentativas");
}
```

## Tratar erros

| Status | `error`             | Quando acontece                                                        |
| ------ | ------------------- | ---------------------------------------------------------------------- |
| `401`  | `Unauthorized`      | Header `X-API-Key` ausente, ou chave inválida, revogada ou expirada.   |
| `403`  | `Forbidden`         | A chave é de outro produto, ou a organização não tem assinatura ativa. |
| `404`  | —                   | O recurso não existe, ou não pertence ao produto da sua chave.         |
| `429`  | `Too Many Requests` | Estourou o limite por minuto, por dia ou a cota mensal do plano.       |
| `500`  | —                   | Erro interno. Se persistir, fale com o suporte.                        |

<Warning>
  O corpo do erro não é uniforme em toda a API — documentamos o que existe hoje,
  não o que gostaríamos que existisse.

  As rotas de gestão devolvem `{ "error", "message", "statusCode" }`. Os erros de
  autenticação e de rate limit devolvem `{ "error", "message" }`, sem
  `statusCode`. E o `404` das rotas de questão devolve só
  `{ "error": "Questão não encontrada" }`.

  Ou seja: **trate pelo status HTTP**, que é consistente, e use `error` /
  `message` apenas para exibir ou registrar em log.
</Warning>

Um wrapper que cobre o caso comum:

```js theme={null}
async function enemhub(caminho, params = {}) {
  const query = new URLSearchParams(params);
  const res = await fetch(`https://api.enemhub.com.br${caminho}?${query}`, {
    headers: { "X-API-Key": process.env.ENEMHUB_API_KEY },
  });

  if (res.ok) return res.json();

  // Trate pelo status: o corpo do erro não tem formato único na API.
  const corpo = await res.json().catch(() => ({}));
  throw Object.assign(
    new Error(corpo.message ?? corpo.error ?? `HTTP ${res.status}`),
    { status: res.status },
  );
}
```
