{
  "info": {
    "name": "Edusati API 1.0",
    "description": "Colecao gerada de docs/api/openapi.yaml. Preencha as variaveis `baseUrl` e `apiKey` (Integracoes > Chaves de API no painel) e as de recurso conforme usar.\n\nDocumentacao: https://docs.edusati.com\n\nNAO edite a mao: regere com `pnpm docs:postman`.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Identidade",
      "item": [
        {
          "name": "Empresa e escopos da chave",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "me"
              ]
            },
            "description": "Primeira chamada de quem integra: com qual empresa estou falando e o que posso\nfazer. Exige apenas uma chave válida — nenhum escopo."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Formulários",
      "item": [
        {
          "name": "Lista os formulários da empresa",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/forms?page=&limit=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "forms"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Escopo: `surveys:read`. Paginação por OFFSET (`page`/`limit`) — a coleção tem\ntamanho de tela e não cresce sem teto como respostas e convites."
          },
          "response": []
        },
        {
          "name": "Formulário com a estrutura (sections/perguntas)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/forms/{{formId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "forms",
                "{{formId}}"
              ]
            },
            "description": "Escopo: `surveys:read`. A estrutura é o que interpreta as chaves de `answers`:\ncada chave de `answers` é o `id` de uma pergunta que aparece em\n`content.sections[].questions[]`."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Respostas",
      "item": [
        {
          "name": "Respostas do formulário",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/forms/{{formId}}/responses?cursor=&limit=&from=&to=&include=&dim.{chave}=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "forms",
                "{{formId}}",
                "responses"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Cursor OPACO da página seguinte, copiado de `meta.nextCursor`. Ausente = primeira\npágina (a única que traz `meta.total`). Não monte nem parseie um: o formato é\ninterno. Cursor ilegível responde 400 `INVALID_CURSOR` - nunca a primeira página.\n`?page=` (paginação antiga desta rota) responde 400 `PAGE_NOT_SUPPORTED`: aceitá-lo\nem silêncio devolveria a primeira página a cada chamada e a varredura ficaria em loop.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "from",
                  "value": "",
                  "description": "Início do período (UTC",
                  "disabled": true
                },
                {
                  "key": "to",
                  "value": "",
                  "description": "Fim do período (UTC",
                  "disabled": true
                },
                {
                  "key": "include",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "dim.{chave}",
                  "value": "",
                  "description": "Recorta por campo de recorte — um parâmetro por campo, todos combinados em E.\nEx.: `?dim.shift=Noite&dim.turno=Integral`. O valor é comparado como string, na\nforma canônica em que foi carimbado.\n\nCampos de REFERÊNCIA (cadastro, unidade, turma) são carimbados com o **id** do\nregistro, não com o rótulo nem com o código: para esses, o filtro é o id\n(`?dim.docente=9f3c…`). Na ESCRITA o código do seu sistema é aceito e resolvido\npara o id; na leitura, não — o código não chega a ser gravado.",
                  "disabled": true
                }
              ]
            },
            "description": "Escopo: `reports:read`. Projeção magra por padrão; `include=answers` traz o payload.\n\nPaginação por CURSOR: chame sem `cursor` e siga o `meta.nextCursor` até\n`meta.hasMore: false`. `meta.total` só vem na primeira página. `?page=` não existe\naqui e responde 400 `PAGE_NOT_SUPPORTED` (em vez de repetir a primeira página).\n\nOrdem: `createdAt` decrescente, desempatado por `id`. Resposta criada **durante** a\nvarredura entra antes do cursor e não aparece nela — use webhooks para o incremental."
          },
          "response": []
        },
        {
          "name": "Uma resposta pelo id",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/responses/{{responseId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "responses",
                "{{responseId}}"
              ]
            },
            "description": "Escopo: `reports:read`. Sempre com `answers` e `metadata`."
          },
          "response": []
        },
        {
          "name": "Agregação pronta do formulário",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/forms/{{formId}}/stats?from=&to=&lang=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "forms",
                "{{formId}}",
                "stats"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "",
                  "description": "Início do período (UTC",
                  "disabled": true
                },
                {
                  "key": "to",
                  "value": "",
                  "description": "Fim do período (UTC",
                  "disabled": true
                },
                {
                  "key": "lang",
                  "value": "",
                  "description": "Idioma dos rótulos gerados (Sim/Não",
                  "disabled": true
                }
              ]
            },
            "description": "Escopo: `reports:read`. Reusa a mesma agregação do dashboard do admin — os números\nda API e os da tela não divergem.\n\nRota CARA (varre as respostas do período). Ela tem teto próprio de rate limit, além\ndo teto da chave: prefira consultá-la sob demanda, não em laço."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Convites",
      "item": [
        {
          "name": "Convites do formulário",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/forms/{{formId}}/invites?cursor=&limit=&status=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "forms",
                "{{formId}}",
                "invites"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Cursor OPACO da página seguinte, copiado de `meta.nextCursor`. Ausente = primeira\npágina (a única que traz `meta.total`). Não monte nem parseie um: o formato é\ninterno. Cursor ilegível responde 400 `INVALID_CURSOR` - nunca a primeira página.\n`?page=` (paginação antiga desta rota) responde 400 `PAGE_NOT_SUPPORTED`: aceitá-lo\nem silêncio devolveria a primeira página a cada chamada e a varredura ficaria em loop.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "Filtra pelo status GRAVADO (não pelo `effectiveStatus`, que é derivado na leitura)",
                  "disabled": true
                }
              ]
            },
            "description": "Escopo: `invites:read`. Cada item traz `url` (link pessoal) e `effectiveStatus`.\n\nPaginação por CURSOR, igual à de respostas."
          },
          "response": []
        },
        {
          "name": "Cria um convite",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\",\n  \"email\": \"contato@exemplo.com\",\n  \"phone\": \"\",\n  \"externalId\": \"\",\n  \"expiresAt\": \"2026-08-01T12:00:00.000Z\",\n  \"dims\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/forms/{{formId}}/invites",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "forms",
                "{{formId}}",
                "invites"
              ]
            },
            "description": "Escopo: `invites:write`. Um contato tem UM convite por formulário — o mesmo e-mail\n(ou telefone) duas vezes responde 409, para a mesma pessoa não receber dois links e\nas duas respostas contarem.\n\nCriar **não envia**: o convite nasce com `status: created` e devolve a `url`. Para\ndespachar por e-mail/WhatsApp, use `POST /forms/{id}/invites/send`; para entregar o\nlink por conta própria, basta a `url`."
          },
          "response": []
        },
        {
          "name": "Envia convites",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"channel\": \"email\",\n  \"inviteIds\": [\n    \"00000000-0000-0000-0000-000000000000\"\n  ],\n  \"batchId\": \"\",\n  \"filter\": {\n    \"statuses\": []\n  },\n  \"templateId\": \"00000000-0000-0000-0000-000000000000\",\n  \"subject\": \"\",\n  \"message\": \"\",\n  \"scheduleAt\": \"2026-08-01T12:00:00.000Z\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/forms/{{formId}}/invites/send",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "forms",
                "{{formId}}",
                "invites",
                "send"
              ]
            },
            "description": "Escopo: `invites:send` (separado de `invites:write` no RBAC porque gasta quota).\nMesmo caminho do admin: quota mensal, supressão, template efetivo e outbox.\n\nO envio é ASSÍNCRONO: a resposta 200 significa \"enfileirado\", não \"entregue\". O\nresultado da entrega chega pelos eventos `invite.sent` / `invite.delivered` /\n`invite.failed`.\n\nEscolha os destinatários por `inviteIds`, `batchId` ou `filter` — sem nenhum dos\ntrês, todos os convites do formulário entram."
          },
          "response": []
        },
        {
          "name": "Revoga um convite",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/invites/{{inviteId}}/revoke",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "invites",
                "{{inviteId}}",
                "revoke"
              ]
            },
            "description": "Escopo: `invites:write`. O link para de funcionar imediatamente. Passa pelo mesmo funil do admin: cancela envios pendentes com refund da quota, apaga o rascunho salvo e emite o evento `invite.revoked`."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Roster acadêmico",
      "item": [
        {
          "name": "Lista as turmas",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/class-groups?status=&code=&page=&limit=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "class-groups"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "code",
                  "value": "",
                  "description": "Código exato da turma no seu sistema.",
                  "disabled": true
                },
                {
                  "key": "page",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "As turmas da instituição, com progresso, situação e tamanho. `size` é o número de\nPOSSÍVEIS AUTORES da turma (o maior entre o tamanho informado e as matrículas\nativas) — é o que sustenta o piso de anonimato dos relatórios."
          },
          "response": []
        },
        {
          "name": "Envia turmas (cria ou atualiza)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rows\": [\n    {}\n  ],\n  \"dryRun\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/class-groups/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "class-groups",
                "import"
              ]
            },
            "description": "Sincroniza turmas pela CHAVE NATURAL `code`: linha com código já cadastrado\nATUALIZA a turma (e a reativa), nunca duplica — reenviar a mesma carga é seguro.\n\nCada linha é validada sozinha: a que for recusada volta em `skipped` com o motivo,\ne o resto do lote entra. Referência por código que não existe (curso, unidade,\ndocente) RECUSA a linha em vez de importar sem ela — turma sem curso vira um buraco\nsilencioso no relatório.\n\nÉ o mesmo contrato da importação por planilha no admin — inclusive o `dryRun`,\nque permite conferir o impacto antes de gravar."
          },
          "response": []
        },
        {
          "name": "Atualiza o progresso da turma",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"progressPct\": 0,\n  \"status\": \"open\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/class-groups/{{id}}/progress",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "class-groups",
                "{{id}}",
                "progress"
              ]
            },
            "description": "Percentual de carga horária concluída. É a escrita mais frequente da integração, e\npor isso tem rota própria: um payload parcial não arrisca zerar o resto da turma.\n\n`id` aceita o identificador da turma **ou o código dela no seu sistema**."
          },
          "response": []
        },
        {
          "name": "Lista as matrículas",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/enrollments?status=&classId=&personId=&changedSince=&cursor=&limit=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "enrollments"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "classId",
                  "value": "",
                  "description": "Id da turma (o `code` vira id em `GET /class-groups?code=`).",
                  "disabled": true
                },
                {
                  "key": "personId",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "changedSince",
                  "value": "",
                  "description": "Só o que mudou de situação a partir deste instante (ISO-8601, inclusivo). É o\nrecorte que torna o polling barato: guarde o horário da última varredura e\nrepasse-o na seguinte.",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Cursor OPACO da página seguinte, copiado de `meta.nextCursor`. Ausente = primeira\npágina (a única que traz `meta.total`). Não monte nem parseie um: o formato é\ninterno. Cursor ilegível responde 400 `INVALID_CURSOR` - nunca a primeira página.\n`?page=` (paginação antiga desta rota) responde 400 `PAGE_NOT_SUPPORTED`: aceitá-lo\nem silêncio devolveria a primeira página a cada chamada e a varredura ficaria em loop.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "A varredura do roster: uma linha por aluno por turma, com a pessoa e a turma\nembutidas — `externalId` e `code` são as chaves que você reconhece, e evitam um\nsegundo GET só para traduzir os nossos ids.\n\nÉ a porta de POLLING da evasão: `?status=dropped&changedSince=…` responde \"quem\nmudou de situação desde X\". O webhook `enrollment.dropped` continua sendo o caminho\npreferido para o incremental — esta rota é a carga inicial e o que salva quem ficou\nfora do ar.\n\nPaginação por CURSOR: chame sem `cursor` e siga o `meta.nextCursor` até\n`meta.hasMore: false`. `meta.total` só vem na primeira página. `?page=` não existe\naqui e responde 400 `PAGE_NOT_SUPPORTED` (em vez de repetir a primeira página).\n\nOrdem: `createdAt` decrescente, desempatado por `id`. Matrícula criada **durante** a\nvarredura entra antes do cursor e não aparece nela."
          },
          "response": []
        },
        {
          "name": "Envia matrículas (cria ou atualiza)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rows\": [\n    {}\n  ],\n  \"dryRun\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/enrollments/import",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "enrollments",
                "import"
              ]
            },
            "description": "Sincroniza matrículas — e as pessoas junto, porque aluno e turma vêm na mesma\nlinha. A pessoa é casada pela matrícula (`externalId`) ou, na falta dela, pelo\ne-mail: reenviar ATUALIZA em vez de duplicar. Linha sem nenhum dos dois é recusada,\nporque criar pessoa só pelo nome duplicaria homônimos a cada envio.\n\nMatrícula que muda para `dropped` dispara o webhook `enrollment.dropped`."
          },
          "response": []
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.edusati.com/v1",
      "type": "string"
    },
    {
      "key": "apiKey",
      "value": "",
      "type": "string"
    },
    {
      "key": "formId",
      "value": "",
      "type": "string"
    },
    {
      "key": "inviteId",
      "value": "",
      "type": "string"
    },
    {
      "key": "responseId",
      "value": "",
      "type": "string"
    }
  ]
}
