API REST — contrato de integração¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-14
Referência interativa da API (OpenAPI/Swagger) → · API Reference (Javadoc) →
Audiência: quem integra um sistema cliente (ERP, job agendado, script) com o
processador. O sistema cliente é qualquer aplicação capaz de mandar HTTP
multipart/form-data sobre HTTPS: job do ERP, script Python, serviço .NET/Java ou
curl. Há também uma tela humana (/processamentos, com login Google), mas o
envio da planilha é só por esta API.
Diferente do projeto irmão Transporte, aqui o período não vai no POST (é
detectado do conteúdo) e o tipo do arquivo é detectado automaticamente (o
cliente não informa tipoArquivo) — ver
detecção do tipo e decisão
0010.
Como enviar as planilhas¶
POST /api/xls/processar — multipart/form-data, autenticado por Bearer. É uma
porta única, de um campo só: arquivo, um .zip contendo de 1 a N .xlsx
do ciclo. O zip é o lote. Não há mais nada a informar: o servidor desempacota,
detecta o tipo de cada planilha, deduplica por checksum, agrupa o lote e agenda o
processamento assíncrono de cada entrada.
O identificador do lote é gerado no servidor como zip-<sha256 do zip>, o que
torna o envio idempotente: reenviar o mesmo zip não abre outro lote nem re-notifica
(decisão 0011).
BASE="https://excel.onpetro.xadm.biz"
TOKEN="<mesmo valor de BI_COMERCIAL_XLS_API_TOKEN no servidor>"
# Empacote os .xlsx do ciclo e mande o zip — um POST só, um campo só.
zip -j lote.zip ./postos_nov2025.xlsx ./margem_nov2025.xlsx
curl -sS -X POST "$BASE/api/xls/processar" \
-H "Authorization: Bearer $TOKEN" \
-F "arquivo=@./lote.zip;type=application/zip"
No Windows (curl.exe, PowerShell/cmd): mesma chamada, trocando a quebra de
linha \ por ^ (cmd) ou ` (PowerShell), e usando aspas duplas nos
campos -F.
Python (requests):
import requests
BASE = "https://excel.onpetro.xadm.biz"
TOKEN = "seu-token" # ideal: variável de ambiente
with open("lote.zip", "rb") as f:
r = requests.post(
f"{BASE}/api/xls/processar",
headers={"Authorization": f"Bearer {TOKEN}"},
files={"arquivo": ("lote.zip", f, "application/zip")},
timeout=120)
r.raise_for_status(); print(r.json())
# {"conjuntoId": "zip-3f9a…", "total": 2, "itens": [
# {"nome": "postos_nov2025.xlsx", "id": 42, "httpStatus": 202,
# "tipoArquivo": "POSTOS", "mensagem": "Processamento agendado"}, ...]}
Respostas do POST¶
O POST responde pelo lote, não por arquivo:
| HTTP | Situação | Corpo |
|---|---|---|
202 |
Lote recebido; processamento agendado por arquivo | conjuntoId (zip-<sha256>), total, itens[] |
400 |
Zip inválido, vazio ou sem nenhum .xlsx |
erro RFC 7807 |
401 |
Bearer ausente ou inválido | — |
O destino de cada planilha vem em itens[], um objeto por entrada do zip
(nome, id, httpStatus, tipoArquivo, mensagem). O httpStatus por item:
httpStatus |
Situação |
|---|---|
202 |
Enfileirado para processamento assíncrono — inclusive o reenvio de um arquivo cujo envio anterior terminou em ERRO ou IGNORADO: o mesmo registro (mesmo id) volta para a fila, no lote novo |
200 |
Duplicado — mesmo checksum já processado com sucesso (idempotência) |
400 |
Não processado — não-.xlsx, nome temporário ~$/#, ou sem coluna reconhecível pelo ArquivoDetector |
409 |
Não processado — mesmo checksum já em fila / processando |
Ou seja: um arquivo recusado não derruba o lote — o 202 do envelope convive com
itens em 400/409.
Todo erro do /api/** (400/401/403/404/409/503, falhas de validação e
rotas inexistentes) sai no formato RFC 7807 (application/problem+json),
decisão 0015:
{ "type": "about:blank", "title": "Conflict", "status": 409,
"detail": "Mesmo checksum já em processamento", "code": "CONFLICT" }
status é o código HTTP, title o resumo, detail a mensagem específica e code
(extensão) é o código estável legível por máquina. O 2xx mantém o corpo de
negócio (statusProcessamento etc.), nunca o code.
Chaves sempre presentes. Todo campo das respostas JSON de sucesso do /api/** vem no corpo
mesmo quando não tem valor — null para escalar ausente (ex.: "id": null num item recusado,
"periodoInicial": null antes do processamento), [] para lista vazia (ex.: "content": [] numa
página sem resultado). A chave nunca é omitida. Ainda assim, trate chave ausente como vazio no
cliente: é a regra da casa para qualquer produtor.
O processamento é assíncrono: para cada item que voltou 202, faça polling em
GET …/processamentos/{id} (o id do próprio item) até SUCESSO/ERRO, ou apenas
acompanhe pelo Telegram — o resumo do lote é postado quando todas as entradas terminam.
Leitura (GET, autenticada)¶
As consultas de processamento exigem autenticação: o Bearer (o mesmo do envio) para
integração, ou a sessão do login Google das telas. Sem nenhum dos dois → 401.
GET /api/comercial/processamentos?page=0&size=20&status=SUCESSO&tipo=META— lista paginada (JSON).statusetiposão filtros opcionais que combinam (AND);tipoaceita qualquer valor deTipoArquivo(COMPRAS, COTA_PETROBRAS, FLUXO_PAGAR_RECEBER, POSTOS, TRR, RELATORIO18, MARGEM_CONSOLIDADA, MARGEM_DIA, META, META_FERNANDO).pagecomeça em 0;sizetem default 20 e máximo 100 (acima disso vale 100; zero ou negativo cai no default). Ordenação fixa porrecebido_emdecrescente — o parâmetrosorté ignorado. Corpo: oPagedo micronaut-data, sem DTO próprio (forma)GET /api/comercial/processamentos/{id}— detalhe (JSON, inclui as métricaslinhas_efetivas/linhas_removidas)GET /api/comercial/processamentos/{id}/logs— log de execução (text/plain)
Interface humana (HTML, protegida por login Google — ver
etapa 06): GET /processamentos,
GET /processamentos/{id} e GET /processamentos/{id}/arquivo — o .xlsx original, em qualquer
modo de storage (no PSQL lê de arquivo_bytes, senão do Garage).
Corpo das listagens paginadas¶
As duas listagens do /api (processamentos e histórico do nuke) devolvem o Page do micronaut-data como
ele se serializa, sem DTO de paginação próprio:
{
"content": [ { "id": 4, "nomeArquivo": "a.xlsx", "status": "SUCESSO", "…": "…" } ],
"pageable": {
"size": 20,
"number": 0,
"sort": { "orderBy": [ { "ignoreCase": false, "direction": "DESC", "property": "recebidoEm",
"ascending": false } ] },
"mode": "OFFSET"
},
"totalSize": 1
}
| Chave | Conteúdo |
|---|---|
content |
itens da página ([] quando vazia) |
totalSize |
total de registros do filtro |
pageable.number |
página devolvida, base 0 |
pageable.size |
tamanho aplicado, já com o corte (default 20, máximo 100) |
pageable.sort.orderBy |
a ordenação que valeu — a fixa da rota, não o sort pedido |
pageable.mode |
OFFSET |
O total de páginas não vem no corpo: é ceil(totalSize / pageable.size). O Page serializado também não
traz totalPages, pageNumber, offset, numberOfElements nem empty.
Import manual de XLS (página aberta) — contrato com o bi-comercial¶
O botão Importar XLS do bi-comercial abre, em nova aba, a página aberta
GET /xls/importar deste app — sem login Google (o usuário já está autenticado no
bi-comercial). A rota /compras/importar é alias da mesma página e continua viva, porque é
nela que o botão já deployado aponta. A identidade e a autorização viajam na URL:
https://excel.onpetro.xadm.biz/xls/importar?u=<userId>&n=<nome>&k=<segredo>
| Param | Conteúdo | Obrigatório |
|---|---|---|
u |
id do usuário logado no bi-comercial | ✅ (sem → 400) |
n |
nome do usuário (url-encoded) | ✅ (sem → 400) |
k |
segredo compartilhado (env BI_COMERCIAL_XLS_IMPORT_TOKEN, mesmo valor embutido no bi-comercial) |
✅ (ausente/errado → 403) |
A página aceita 1..N .xlsx de Relação de Compras (COMPRAS), Cota Petrobrás
(COTA_PETROBRAS) e FluxoPagarReceber (FLUXO_PAGAR_RECEBER) — outro tipo → 400 — com no
máximo uma planilha de cota (o full-replace assume a matriz inteira) e no máximo um
FluxoPagarReceber por envio (duas → 400). Ela empacota os arquivos no .zip do lote no
servidor, reusa o pipeline de POST /api/xls/processar e grava enviado_por_id/enviado_por_nome
no xls_processamento (visíveis no detalhe). Sem BI_COMERCIAL_XLS_IMPORT_TOKEN configurado no
servidor, a página recusa tudo (403, fail-closed).
FluxoPagarReceber: a página espera o processamento (consulta a cada app.importacao.intervalo-espera,
500 ms, por até app.importacao.espera-maxima, 30 s) e mostra o desfecho real, porque o usuário do
cliente não acessa o histórico de processamentos. Compras e Cota só agendam, como antes.
| Desfecho | O que a página mostra |
|---|---|
SUCESSO |
"Posição dd/mm/aaaa — passou a ser a posição atual" (ou "entrou no histórico (já existe posição mais recente de …)" ou "substituiu a importação anterior desta posição"); "A pagar/A receber: N títulos · R$ X · vencido R$ Y"; avisos de centros de custo e filiais novos e de divergência do total do receber. Vem do xls_processamento.resumo |
Duplicado (200) |
o mesmo resumo, gravado na importação original, com "(importado em dd/mm/aaaa hh:mm)" |
IGNORADO |
"Arquivo recusado — nada foi gravado." + o motivo (erro_mensagem: aba ausente, posição ausente, ou as linhas inválidas) |
ERRO |
"Falha ao processar o arquivo — a equipe foi avisada; tente reenviar mais tarde." (o detalhe vai ao GlitchTip) |
| estourou a espera | "Ainda processando — a posição nova aparece no BI assim que terminar." |
Segurança (consciente): endpoint aberto protegido por segredo compartilhado client-grade — não é auth forte. Aceitável para ferramenta interna; o id do usuário é rastro de autoria, não credencial. Não replicar esse padrão em endpoint sensível sem avaliar. As envs estão no runbook de deploy.
Operação admin (Bearer + confirmação)¶
POST /api/comercial/admin/nuke-replication— dispara o reset da replicação PowerSync. Exige Bearer + headerX-Confirm-Nuke(valor dePOWERSYNC_NUKE_CONFIRMATION). Retorna202comnukeId+linkStatus. Operação destrutiva — o procedimento completo (steps, polling, recovery) está no runbook Resetar a replicação.GET /api/comercial/admin/nuke-replication?page=0&size=20— histórico das execuções (Bearer), compage/sizenos mesmos limites da listagem de processamentos e ordenação fixa poriniciado_emdecrescente. Corpo: o mesmoPage(forma), cada item na forma do status de uma execução (nukeId,status,stepsCompletados, …).
Limites e boas práticas¶
- Respeitar
micronaut.server.multipart.max-file-size(50 MB no exemplo do projeto — ajustável). O limite vale para o zip inteiro, não por planilha. - HTTPS sempre em produção.
- Em item
409, aguardar o término do processamento em andamento (ou consultar oidna lista). Arquivo que terminou emERRO/IGNORADOpode ser reenviado como está: ele é reprocessado (202). - Em item
400recorrente, conferir o nome do arquivo (sem~$/#) e se o conteúdo tem uma coluna reconhecível peloArquivoDetector. - Swagger UI interativo:
https://<host>/swagger-ui(botão Authorize para o Bearer).