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/coraEscopos 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.
| Escopo | Endpoints cobertos |
|---|---|
cora:write | POST /consentimento/iniciar, DELETE /integracao |
cora:read | GET /integracao/status, GET /extrato |
cora:sync | POST /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âmetro | Tipo | Local | Obrigatório | Descrição |
|---|---|---|---|---|
code | str | query | sim | Authorization code do Cora (expira em ~60s). |
state | str | query | sim | State 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 (stateinvá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âmetro | Tipo | Local | Obrigatório | Descrição |
|---|---|---|---|---|
cliente_id | int | query | sim | ID 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âmetro | Tipo | Local | Obrigatório | Descrição |
|---|---|---|---|---|
cliente_id | int | query | sim | ID do cliente (> 0). |
start | str | query | não | Filtro de data inicial no formato YYYY-MM-DD. |
end | str | query | não | Filtro de data final no formato YYYY-MM-DD. |
page | int | query | não | Página (>= 1, default 1). |
per_page | int | query | não | Itens 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âmetro | Tipo | Local | Obrigatório | Descrição |
|---|---|---|---|---|
cliente_id | int | query | sim | ID 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
| Campo | Tipo | Obrigatório | Observações |
|---|---|---|---|
cliente_id | int | sim | ID do cliente (customer.id, > 0). |
CoraConsentimentoIniciarResponse
| Campo | Tipo | Observações |
|---|---|---|
authorization_url | str | URL do Cora para onde o cliente deve ser redirecionado. |
state | str | State assinado (anti-CSRF), de curta validade. |
expires_in | int | Validade do state, em segundos. |
CoraIntegracaoStatus
Schema mascarado - nunca expõe tokens de acesso/refresh.
| Campo | Tipo | Notas |
|---|---|---|
cliente_id | int | ID do cliente. |
status | str | nao_conectado, pendente, ativo, revogado ou expirado. |
conectado_em | datetime? | Quando o consentimento ficou ativo (created/updated). |
ultimo_sync | datetime? | Data/hora da última sincronização. |
consentimento_expira_em | datetime? | Fim da janela de 60 dias de inatividade do consentimento. |
CoraSincronizarRequest
| Campo | Tipo | Obrigatório | Observações |
|---|---|---|---|
cliente_id | int | sim | ID do cliente (> 0). |
start | date | sim | Data inicial (YYYY-MM-DD). |
end | date | sim | Data final (YYYY-MM-DD); deve ser > start. |
CoraSincronizarResponse
| Campo | Tipo | Notas |
|---|---|---|
novos | int | Lançamentos novos inseridos. |
duplicados | int | Lançamentos já existentes (ignorados pela deduplicação). |
total_periodo | int | Total de lançamentos no período. |
saldo_inicial | float | Saldo no início do período, em reais. |
saldo_final | float | Saldo no fim do período, em reais. |
periodo | object | { start, end } (CoraSincronizarPeriodo). |
CoraExtratoLancamentoRead
| Campo | Tipo | Notas |
|---|---|---|
id | int | Identificador interno do lançamento. |
cliente_id | int | Cliente vinculado. |
cora_entry_id | str | ID do lançamento no Cora (chave de deduplicação). |
tipo | str? | Tipo do lançamento (ex.: CREDIT, DEBIT). |
valor | float? | Valor do lançamento, em reais. |
data_lancamento | datetime? | Data/hora do lançamento. |
transacao_id | str? | ID da transação associada. |
transacao_tipo | str? | Tipo da transação (ex.: PIX, TED, BOLETO). |
descricao | str? | Descrição do lançamento. |
contraparte_nome | str? | Nome da contraparte. |
contraparte_documento | str? | Documento (CPF/CNPJ) da contraparte. |
saldo | float? | Saldo após o lançamento, em reais. |
criado_em | datetime | Quando 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 é oGET /consentimento/callback, que devolve HTML. - Valores monetários são expostos em reais (
floatna borda da API). A persistência usaDecimal/Numeric, e a conversão centavos → reais é feita no service comDecimal(nuncafloat). - Tokens de acesso/refresh são sensíveis e nunca aparecem em nenhum schema de leitura;
CoraIntegracaoStatusé mascarado (só status e datas). GET /extratolê do nosso banco (dados já sincronizados); para trazer lançamentos novos do Cora, rode antesPOST /extrato/sincronizar.- O consentimento tem janela de 60 dias de inatividade; expirado ou revogado, a sincronização retorna
409e é preciso reiniciar o fluxo emPOST /consentimento/iniciar.