Banco Cora: Extratos

Integração da WebApiAlcance com o Banco Cora para captura de extratos bancários dos clientes. O fluxo cobre o consentimento OAuth2 (o cliente autoriza a Alcance a ler seus extratos), o callback público de retorno do Cora, a consulta do status (mascarado) da integração, a sincronização dos lançamentos do período e a listagem dos lançamentos já persistidos no nosso banco. Cobre também a revogação da integração.

Autenticação e escopos por-rota

Este domínio é montado sem o guard global de escopo - cada rota declara seu próprio escopo (cora:read, cora:write ou cora:sync). A maioria dos endpoints exige Authorization: Bearer <API_KEY> com o escopo correspondente. A exceção é o callback de consentimento, que é público: ele valida o state assinado (anti-CSRF), não a sessão, porque quem o acessa é o navegador do cliente redirecionado pelo Cora. Veja Autenticação para o fluxo de API Key. O Swagger oficial está em api.contabilidadealcance.com.br/docs.

Base path

/api/v1/cora

Escopos necessários

Diferente da maioria dos domínios, o escopo não é aplicado por um guard global - cada rota o declara explicitamente via scope_required(...). Note que cora:sync não é derivável do método/rota (um POST derivaria write): é exigido de forma explícita para a sincronização.

EscopoEndpoints cobertos
cora:writePOST /consentimento/iniciar, DELETE /integracao
cora:readGET /integracao/status, GET /extrato
cora:syncPOST /extrato/sincronizar (escopo explícito)
(público)GET /consentimento/callback - sem auth; valida o state assinado

Rota pública: /consentimento/callback

GET /cora/consentimento/callback é PÚBLICA - não exige API Key nem escopo. Ela é acessada pelo navegador do cliente, redirecionado pelo Cora após a autorização. A proteção é o state assinado emitido no iniciar (anti-CSRF, curta validade), não a sessão. A resposta é uma página HTML (sucesso ou erro), não um envelope JSON. Nunca registre (log) o code nem o state - são sensíveis.

Endpoints

POST /cora/consentimento/iniciar

Inicia o consentimento OAuth2 de um cliente e gera a URL de autorização do Cora, para onde o cliente deve ser redirecionado. O state retornado é assinado (anti-CSRF) e tem validade curta.

Parâmetros

Este endpoint não recebe parâmetros de rota ou query - os dados vão no corpo da requisição (CoraConsentimentoIniciarRequest): cliente_id (obrigatório).

Request

{
  "cliente_id": 487
}

Response 200 OK

{
  "success": true,
  "message": "Consentimento iniciado. Redirecione o cliente.",
  "data": {
    "authorization_url": "https://oauth.cora.com.br/authorize?client_id=...&state=...",
    "state": "eyJhbGciOi...",
    "expires_in": 300
  }
}

Erros: 503 (integração Cora não configurada), 404 (cliente não encontrado), 422 (dado inválido).

Escopo: cora:write

GET /cora/consentimento/callback

Rota pública. Endpoint de retorno (redirect) do Cora após o cliente autorizar. Valida o state assinado, troca o code por tokens e ativa a integração. Retorna uma página HTML (text/html) de sucesso ou erro - não um envelope JSON.

Parâmetros

ParâmetroTipoLocalObrigatórioDescrição
codestrquerysimAuthorization code do Cora (expira em ~60s).
statestrquerysimState assinado emitido em /consentimento/iniciar.

Request

Sem corpo de requisição.

Response 200 OK (HTML)

A resposta é uma página HTML (text/html), não um envelope JSON:

  • 200 OK - HTML "Conta conectada com sucesso".
  • 400 Bad Request - HTML de erro (state inválido/expirado ou consentimento recusado).
  • 502 Bad Gateway - HTML de erro (falha de comunicação com o Cora).

Escopo: nenhum - rota pública (protegida pelo state assinado).

GET /cora/integracao/status

Retorna o status mascarado da integração Cora de um cliente. Nunca expõe tokens - apenas situação e datas.

Parâmetros

ParâmetroTipoLocalObrigatórioDescrição
cliente_idintquerysimID do cliente (> 0).

Request

Sem corpo de requisição.

Response 200 OK

{
  "success": true,
  "message": "Status da integracao Cora.",
  "data": {
    "cliente_id": 487,
    "status": "ativo",
    "conectado_em": "2026-06-10T14:22:05",
    "ultimo_sync": "2026-07-02T03:00:11",
    "consentimento_expira_em": "2026-08-31T14:22:05"
  }
}

Quando o cliente nunca consentiu, status = nao_conectado e as datas vêm null.

Escopo: cora:read

POST /cora/extrato/sincronizar

Sincroniza o extrato Cora do cliente para o período informado: faz refresh do token, busca os lançamentos no Cora e persiste com deduplicação (não reinsere lançamentos já existentes).

Parâmetros

Este endpoint não recebe parâmetros de rota ou query - os dados vão no corpo da requisição (CoraSincronizarRequest): cliente_id (obrigatório), start e end (obrigatórios, end deve ser estritamente maior que start).

Request

{
  "cliente_id": 487,
  "start": "2026-06-01",
  "end": "2026-06-30"
}

Response 200 OK

{
  "success": true,
  "message": "Extrato sincronizado com sucesso.",
  "data": {
    "novos": 42,
    "duplicados": 8,
    "total_periodo": 50,
    "saldo_inicial": 1250.75,
    "saldo_final": 3890.10,
    "periodo": { "start": "2026-06-01", "end": "2026-06-30" }
  }
}

Erros: 409 (consentimento expirado/revogado - é preciso reconsentir), 503 (Cora não configurado), 502 (falha de comunicação com o Cora), 404/422 (cliente inválido / dado inválido).

Escopo: cora:sync (escopo explícito, não derivável da rota)

GET /cora/extrato

Lista, de forma paginada, os lançamentos de extrato Cora já persistidos no nosso banco (não consulta o Cora em tempo real - use POST /extrato/sincronizar para atualizar).

Parâmetros

ParâmetroTipoLocalObrigatórioDescrição
cliente_idintquerysimID do cliente (> 0).
startstrquerynãoFiltro de data inicial no formato YYYY-MM-DD.
endstrquerynãoFiltro de data final no formato YYYY-MM-DD.
pageintquerynãoPágina (>= 1, default 1).
per_pageintquerynãoItens por página (1-500, default 100).

Datas fora do formato YYYY-MM-DD retornam 422.

Request

Sem corpo de requisição.

Response 200 OK

{
  "success": true,
  "message": "Lancamentos recuperados com sucesso.",
  "data": [
    {
      "id": 5012,
      "cliente_id": 487,
      "cora_entry_id": "ent_abc123",
      "tipo": "CREDIT",
      "valor": 1500.00,
      "data_lancamento": "2026-06-12T09:31:00",
      "transacao_id": "trx_998",
      "transacao_tipo": "PIX",
      "descricao": "Recebimento cliente XPTO",
      "contraparte_nome": "XPTO Comercio LTDA",
      "contraparte_documento": "12345678000199",
      "saldo": 3890.10,
      "criado_em": "2026-07-02T03:00:11"
    }
  ]
}

Escopo: cora:read

DELETE /cora/integracao

Revoga a integração Cora de um cliente, limpando os tokens armazenados. Após a revogação, sincronizar volta a exigir um novo consentimento.

Parâmetros

ParâmetroTipoLocalObrigatórioDescrição
cliente_idintquerysimID do cliente (> 0).

Request

Sem corpo de requisição.

Response 200 OK

Cliente inexistente retorna 404.

{
  "success": true,
  "message": "Integracao Cora revogada.",
  "data": null
}

Escopo: cora:write

Schemas

CoraConsentimentoIniciarRequest

CampoTipoObrigatórioObservações
cliente_idintsimID do cliente (customer.id, > 0).

CoraConsentimentoIniciarResponse

CampoTipoObservações
authorization_urlstrURL do Cora para onde o cliente deve ser redirecionado.
statestrState assinado (anti-CSRF), de curta validade.
expires_inintValidade do state, em segundos.

CoraIntegracaoStatus

Schema mascarado - nunca expõe tokens de acesso/refresh.

CampoTipoNotas
cliente_idintID do cliente.
statusstrnao_conectado, pendente, ativo, revogado ou expirado.
conectado_emdatetime?Quando o consentimento ficou ativo (created/updated).
ultimo_syncdatetime?Data/hora da última sincronização.
consentimento_expira_emdatetime?Fim da janela de 60 dias de inatividade do consentimento.

CoraSincronizarRequest

CampoTipoObrigatórioObservações
cliente_idintsimID do cliente (> 0).
startdatesimData inicial (YYYY-MM-DD).
enddatesimData final (YYYY-MM-DD); deve ser > start.

CoraSincronizarResponse

CampoTipoNotas
novosintLançamentos novos inseridos.
duplicadosintLançamentos já existentes (ignorados pela deduplicação).
total_periodointTotal de lançamentos no período.
saldo_inicialfloatSaldo no início do período, em reais.
saldo_finalfloatSaldo no fim do período, em reais.
periodoobject{ start, end } (CoraSincronizarPeriodo).

CoraExtratoLancamentoRead

CampoTipoNotas
idintIdentificador interno do lançamento.
cliente_idintCliente vinculado.
cora_entry_idstrID do lançamento no Cora (chave de deduplicação).
tipostr?Tipo do lançamento (ex.: CREDIT, DEBIT).
valorfloat?Valor do lançamento, em reais.
data_lancamentodatetime?Data/hora do lançamento.
transacao_idstr?ID da transação associada.
transacao_tipostr?Tipo da transação (ex.: PIX, TED, BOLETO).
descricaostr?Descrição do lançamento.
contraparte_nomestr?Nome da contraparte.
contraparte_documentostr?Documento (CPF/CNPJ) da contraparte.
saldofloat?Saldo após o lançamento, em reais.
criado_emdatetimeQuando o lançamento foi persistido no nosso banco.

Notas

  • Toda resposta JSON segue o envelope { success, message, data } (ou { success: false, message, details } em erro). A exceção é o GET /consentimento/callback, que devolve HTML.
  • Valores monetários são expostos em reais (float na borda da API). A persistência usa Decimal/Numeric, e a conversão centavos → reais é feita no service com Decimal (nunca float).
  • Tokens de acesso/refresh são sensíveis e nunca aparecem em nenhum schema de leitura; CoraIntegracaoStatus é mascarado (só status e datas).
  • GET /extrato lê do nosso banco (dados já sincronizados); para trazer lançamentos novos do Cora, rode antes POST /extrato/sincronizar.
  • O consentimento tem janela de 60 dias de inatividade; expirado ou revogado, a sincronização retorna 409 e é preciso reiniciar o fluxo em POST /consentimento/iniciar.