API do Integrador — /api/v1/xadm¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10
Como o maxsul-pied grava o estado desejado nas tabelas espelho do X-Adm
(contratos/propriedades/fones/itensped/estoque): pelo mesmo contrato REST que o agente
on-premises do X-Adm já usa para popular o Integrador. O maxsul-pied não escreve SQL direto —
mesmo compartilhando o Postgres, respeita a propriedade do Integrador (Projeto
§2.2). O schema autoritativo das tabelas vive no repositório do Integrador.
1. O contrato de ingestão¶
O contrato abaixo é fato do Integrador, que é o dono dele: chega do raw/ dele vendorado em
docs/_importado/, byte a byte, fixado (pin) na versão que este app implementa — não re-digitado
(constituição §1.9). Adotar versão nova do contrato é atualizar a cópia no mesmo PR que muda o código.
Por que isto não é uma cópia
Até 2026-07-17 esta página re-digitava o contrato à mão, e ele já havia envelhecido: dizia
que nenhuma origem da PIED estava registrada no Integrador (a INT-PIED já estava), e não
trazia a guarda de no-op nem os casos 401/500. Fato re-digitado longe do dono envelhece
sozinho; fato importado, não.
Endpoint e autenticação¶
O integrador tem um endpoint de ingestão, com dois verbos sobre o mesmo corpo:
| Verbo | Efeito |
|---|---|
PUT /api/v1/xadm |
upsert — cria ou atualiza; registro que existia deletado é revivido |
DELETE /api/v1/xadm |
soft delete dos registros identificados pelas chaves de negócio do corpo |
Toda rota /api/** exige Authorization: Bearer <token>. O token é estático e por
instalação (há uma instalação por cliente), entregue por canal seguro — sem ele a
resposta é 401.
Envelope¶
O corpo é um objeto JSON com o campo Origem (string) e uma chave por tabela, cada
uma com um array de objetos:
{
"Origem": "PNOTAI",
"ESTOQUE": [ { "ChaveEst": "...", "Saldo": 10 } ],
"MUNICIPIO": [ { "CodMun": "4314902", "Estado": "RS", "NomeMun": "Porto Alegre" } ]
}
As chaves de tabela reconhecidas são 15:
CONTRATOS · ITENSPED · ITENSPEDAMX · ITPEDAMXDI · LOTEEST · ESTOQUE ·
MUNICIPIO · PROPRIEDADES · FONES · VEICULOS · NOTACOMPL · ITENS ·
ITENSLOTE · TBPCOEST · FORMULAS
O nome da chave é case-sensitive: chave com caixa errada não casa tabela nenhuma e o payload é tratado como vazio (ver Resposta). Tabela ausente do corpo é ignorada — só se envia o que mudou.
Resposta¶
O status HTTP não diz se deu certo. Falha de processamento responde 200 com
resultado: "ERRO" — o contrato foi mantido assim por compatibilidade com o integrador
antigo. Quem consome tem de olhar o campo resultado, não o status HTTP; um cliente
que trate 200 como sucesso perde toda falha em silêncio.
{ "requestId": 1234, "resultado": "SUCESSO", "mensagem": null }
| Campo | Valor |
|---|---|
requestId |
id do registro Request criado para esta chamada (rastreia o processamento) |
resultado |
SUCESSO, ERRO ou PENDENTE |
mensagem |
null em sucesso; o detalhe do erro quando resultado é ERRO |
| HTTP | Quando |
|---|---|
200 |
o corpo era JSON válido — inclusive quando o processamento falhou (resultado: "ERRO") |
400 |
JSON inválido ou malformado; corpo {"requestId": null, "resultado": null, "mensagem": "JSON inválido: …"} |
401 |
Bearer ausente ou inválido |
500 |
falha antes do processamento (ex.: o registro da chamada não gravou). Erro de processamento não é 500 — é 200 com ERRO |
Payload que não casa nenhuma tabela conhecida responde 200 com resultado: "ERRO" e a
mensagem "payload sem nenhuma tabela reconhecida (verifique as chaves de array por
tabela)". É guarda deliberada contra o no-op silencioso: sem ela, chave com caixa errada
voltaria SUCESSO sem gravar nada.
Origem¶
Origem identifica a rotina do X-Adm que originou a carga. Ela é gravada e usada para
rastreio, nunca validada: origem desconhecida é aceita e processada normalmente.
O que a lista de origens conhecidas muda é só o roteamento de erro ao Sentry — falha
de processamento numa origem conhecida vira evento com as tags origem/tipo/request_id;
numa origem fora da lista, a falha não é reportada. Ou seja, origem errada não quebra a
chamada, mas apaga o alarme: o erro fica invisível.
Origens conhecidas hoje: PPEDAMX · PPEDIDO · PBLDI · PNOTAI · PNOTAD ·
PCSTPROD · PREQUIS · INT-PIED.
FORMULAS — fórmula (BOM) do kit (fluxo de entrada PIED)¶
Tabela de entrada (Origem: INT-PIED): a fórmula de um pedido-kit — um objeto por
componente do kit. O produtor manda só três campos de input (caixa exata):
| Campo | Tipo | Conteúdo |
|---|---|---|
xPed |
string(15) | código do pedido do kit (casa com o xPed de CONTRATOS/ITENSPED) |
CodProd |
string(6) | código do produto componente |
Qtde |
decimal | quantidade do componente consumida na fórmula |
{ "Origem": "INT-PIED",
"FORMULAS": [ { "xPed": "260069986", "CodProd": "22162", "Qtde": 10 },
{ "xPed": "260069986", "CodProd": "16442", "Qtde": 2 } ] }
Chave natural (xPed, CodProd). É fluxo de entrada de mão única para o dado: o X-Adm cadastra
e devolve só o status (CodRetorno/MsgRetorno) pelo write-back (006 = gravado) — nunca
devolve chave, então a tabela não carrega colunas Chave*. Reenvio idempotente preserva o status já
gravado. Sem DELETE (pedido pago é imutável).
Tabela ESTOQUE¶
Cada item do array ESTOQUE é um produto. Todos os campos são opcionais no envelope —
o que não vier não é escrito.
| Campo | Tipo | Nota |
|---|---|---|
ChaveEst |
texto | identidade do produto. É a chave de join do espelho |
codProd |
texto | identidade alternativa, usada onde ChaveEst chega vazio. Aceita CodProd (PascalCase) |
NomeProd |
texto | descrição. Aceita nomeProd |
EAN13 |
texto | código de barras. Aceita ean13 |
CodProdAlt |
texto | código alternativo do produto no X-Adm |
CodigoAlt |
texto | código do produto na PIED — rastreabilidade de origem externa, gravado em pied_codigo_alt. Não é identidade nem critério de join |
Venda |
decimal | preço de venda. Aceita venda |
VendaPz |
decimal | preço a prazo. Aceita vendaPz |
Saldo |
decimal | saldo em estoque. Aceita saldo |
Identidade. O produto é identificado por ChaveEst; onde ele chega vazio (o caso da
instância Thoms), a identidade é o codProd. A chave do parceiro (CodigoAlt) nunca
é identidade — entra prefixada no espelho (pied_codigo_alt) e serve só para rastrear de
onde o dado veio. Promover chave de parceiro a identidade de primeira classe acopla o
espelho ao vocabulário de um terceiro.
Write-back. CodRetorno e MsgRetorno não são lidos deste payload: são escritos
pelo X-Adm no sentido inverso. Mandá-los aqui não tem efeito.
Caixa dos campos. Onde a tabela diz "aceita", o parser lê as duas grafias; nos demais
campos a grafia é exata. Campo com caixa fora do previsto é ignorado em silêncio — o
registro é gravado sem ele, e a chamada volta SUCESSO.
2. Como o maxsul-pied usa esse contrato¶
O que segue não é o contrato — é a decisão deste app sobre como consumi-lo.
- Ordem
DELETE → PUT: o padrão do ERP para limpar/reescrever estado. Omaxsul-piedenvia os pares na ordem certa e não paraleliza writes concorrentes na mesma chave. (Do outro lado, o Integrador serializa o apply num lock global, FIFO — mas depender disso seria depender de detalhe interno do dono; a ordem é responsabilidade de quem envia.) - Soft-delete/revive: combina com o content-hash do transform — só reescreve em mudança real.
- Idempotência: a chave natural (código do pedido/produto, documento do cliente) é o que torna o reenvio do mesmo estado inofensivo.
- Leitura da resposta: o
IntegradorClienttrata sóresultado: "SUCESSO"como enviado; qualquer outro viraERROna máquina de status, sem retentar. O transporte (timeout/5xx/429) é que é retentado. Isto implementa o aviso do contrato acima — o status HTTP não diz se deu certo. - Reconciliação pela remessa: o status do pedido sai do manifesto (§5), não do retorno de uma
tabela —
CONFIRMADOsó quando não há remessa viva e existe ao menos umaENTREGUE. Origem: omaxsul-piedenviaINT-PIEDem todo o payload da Maxsul (já registrada no Integrador — ver Origem no contrato acima; é o que faz a falha destes writes ser reportada).
3. Corpo por tabela (o mapeamento deste app)¶
Payload = Origem + um array por tabela, na ordem de dependência
(propriedades → fones → estoque → contratos → itensped). Nomes de campo conforme
Modelagem §1 (PascalCase do MAXSUL2025_610110). PUT faz upsert por chave
natural; DELETE leva só as chaves.
PUT (upsert):
{
"Origem": "INT-PIED",
"propriedades": [ { "CgcCpf": "12345678000109", "CodProp": " 0", "NomeProp": "Solar Sul Ltda",
"Endereco": "Rua das Acácias", "Numero": "100", "Bairro": "Centro",
"Cidade": "Ponta Grossa", "Estado": " PR", "CEP": "84010000" } ],
"fones": [ { "CgcCpf": "12345678000109", "DDD": "42", "Telefone": "999990000",
"Nome": "João", "Email": "joao@solarsul.com" } ],
"estoque": [ { "CodigoAlt": "MOD-550", "NomeProd": "Painel Solar 550W", "Venda": 780.00000 } ],
"contratos": [ { "xPed": "6f3a9c12", "CgcCpfCliente": "12345678000109", "ChaveUnid": " 1 29",
"CodMat": " 1 0 134", "DtEm": "20260615", "VlTot": 7800.00, "NatOp": "6.102",
"TpVda": "C" } ],
"itensped": [ { "xPed": "6f3a9c12", "CodigoAlt": "MOD-550", "Qtde": 10.000, "Valor": 780.00000,
"Total": 7800.00 } ]
}
DELETE (por chave natural):
{ "Origem": "INT-PIED",
"itensped": [ { "xPed": "6f3a9c12", "CodigoAlt": "MOD-550" } ],
"contratos": [ { "xPed": "6f3a9c12" } ] }
Formatação dos valores: no pipeline viajam tipados (
NUMERIC/TEXT/DATE); a conversão para o formato de armazenamento do ZIM (Character com espaços significativos, VastInt com escala fixa, datasAAAAMMDD) é feita no cliente — ver Tratamento de dados no ZIM.
4. Write-back (status de volta)¶
O resultado da gravação no X-Adm (CodRetorno/MsgRetorno e as Chave*) volta pelo conector
PowerSync do Integrador (POST /api/v1/powersync), que persiste nas tabelas espelho. Ver
Projeto §5.
5. Manifesto de remessa — a consulta de retorno e o fechamento à mão¶
O aceite do PUT (§1) só diz que o Integrador persistiu o estado desejado — não o resultado do
X-Adm. Quem fecha esse laço é o manifesto de remessa: uma consulta cobre o pedido inteiro, com o
status da remessa e, nas TRAVADA, os itens rejeitados. O contrato é fato do dono, vendorado como os
da §1 — pin: integrador-server 43c3048 (decisão 0025, com o 409 para linha pendente).
Consultar remessas¶
GET /api/v1/integracao/remessas?origem=<origem>[&chave=<chave>]...[&status=<status>]
Fora do namespace /xadm de propósito: remessa é dado de controle do Integrador, não espelho do
ERP. Toda rota /api/** exige Authorization: Bearer <token>; sem ele, 401.
| parâmetro | efeito |
|---|---|
origem |
obrigatória — hoje só INT-PIED tem manifesto |
chave |
pied_x_ped do pedido ou cgc_cpf do cliente. Aceita repetição (&chave=A&chave=B) — uma chamada por rodada, não uma por pedido |
status |
filtra por ABERTA, ENTREGUE, TRAVADA ou RESOLVIDA_MANUAL |
Chave inexistente não é 404: é 200 com lista vazia. Quem consome decide pelo conteúdo.
[
{ "id": "0199…", "chaveNegocio": "260079421", "status": "TRAVADA", "totalItens": 5,
"criadoEm": "2026-09-08T14:02:11Z",
"itensRejeitados": [
{ "tabela": "propriedades", "linhaId": "0199…", "codRetorno": "120",
"msgRetorno": "Cliente CPF 072.508.149-07 não cadastrado.", "bloqueado": false },
{ "tabela": "contratos", "linhaId": "0199…", "codRetorno": "120",
"msgRetorno": "Pedido (10738.24) não cadastrado.", "bloqueado": true } ] },
{ "id": "0199…", "chaveNegocio": "260081234", "status": "RESOLVIDA_MANUAL", "totalItens": 4,
"criadoEm": "2026-09-09T10:40:02Z", "itensRejeitados": [],
"resolucaoManual": { "em": "2026-09-10T16:20:00Z",
"observacao": "Lançado manualmente no X-Adm: kit sem produto cadastrado.",
"operador": "fulano@maxsul.com.br" } }
]
| campo | valor |
|---|---|
id |
id da remessa |
chaveNegocio |
pied_x_ped ou cgc_cpf |
status |
ver a tabela de estados abaixo |
totalItens |
quantos itens a remessa declara — a coluna, não um COUNT(*); é o que detecta lista rasgada |
criadoEm |
quando a remessa nasceu |
itensRejeitados |
os itens em 1XX, só nas TRAVADA. Nas demais vem [] — nunca ausente |
resolucaoManual |
só nas RESOLVIDA_MANUAL: em, observacao e operador (este some quando não foi informado). Nas demais, o campo some |
Em cada item rejeitado vêm tabela, linhaId, codRetorno, msgRetorno e bloqueado. O
bloqueado separa a causa do colateral: true quer dizer que um pai desta linha, na mesma
remessa, também está em 1XX. Quem reporta ao operador nomeia a mensagem do item não
bloqueado. No exemplo, a causa é o cliente; o "Pedido não cadastrado" é consequência.
Os estados¶
status |
significa | viva? |
|---|---|---|
ABERTA |
ainda não entregue, sem rejeição terminal | sim |
TRAVADA |
alguma linha em 1XX; só o re-arme a reabre |
sim |
ENTREGUE |
todas as linhas aceitas (006) — histórico |
não |
RESOLVIDA_MANUAL |
o operador lançou o pedido à mão no X-Adm e fechou a remessa por comando — histórico | não |
Uma remessa viva por (origem, chave): é invariante de banco. As não vivas acumulam. Os três
primeiros estados são derivados dos cod_retorno. RESOLVIDA_MANUAL é o único que nasce de
comando e que a derivação nunca reabre.
Um consumidor que encontre um status que não conhece deve tratá-lo como não travado e não enviável. É o que mantém a adição de um estado não quebrante.
Fechar à mão uma remessa travada¶
POST /api/v1/integracao/remessas/resolver-manual?origem=INT-PIED&chave=<xPed>
Content-Type: application/json
{ "observacao": "Lançado manualmente no X-Adm: kit sem produto cadastrado.",
"operador": "fulano@maxsul.com.br" }
Serve para o pedido que não se resolve reenviando (ex.: o produto-kit que o ERP não criou) e que o
operador lançou direto no X-Adm. Sem isto, a remessa seguiria TRAVADA para sempre. Quem chama é o
botão "Importado manualmente" do painel do maxsul-pied.
| corpo | regra |
|---|---|
observacao |
obrigatória, até 2000 caracteres — é o que sobra para reconstruir o caso |
operador |
opcional, até 100 caracteres. O Bearer é estático e não identifica pessoa |
{ "chave": "260079421", "resolvidas": 1 }
| HTTP | quando |
|---|---|
200 |
fechou (resolvidas: 1), ou não havia remessa viva (resolvidas: 0: nunca existiu, já entregue ou já resolvida) |
400 |
origem ausente ou diferente de INT-PIED; chave ausente; observacao ausente, branca ou longa demais; operador longo demais |
401 |
Bearer ausente ou inválido |
409 |
a remessa viva está ABERTA (ainda em entrega), está TRAVADA com linha ainda pendente (000/001/9XX), ou mudou de estado durante o comando — consulte e tente de novo |
Só TRAVADA. Fechar uma ABERTA enquanto o cliente pode estar mandando as linhas arrisca
duplicar no ERP.
E sem linha pendente. Numa TRAVADA o filho de um pai 1XX fica 000, bloqueado. Fechar a
remessa a tira do manifesto, e o cliente passaria a mandar esse filho por presença — duplicando no
ERP o que o operador lançou à mão. Nesse caso o caminho é corrigir o dado e re-armar.
Não mexe nas linhas. As 1XX continuam 1XX e o cliente não as coleta. Muda só a remessa.
Idempotente: a segunda chamada devolve resolvidas: 0, porque a remessa já não está viva.
Depois dele, o re-arme da chave responde 409. Re-armar reenviaria ao ERP o que o operador já
lançou. E se a chave ganhar uma remessa viva nova, o re-arme dela não reabre as linhas de pedido
(contratos/itensped/formulas) que também são itens da resolvida.
Como o maxsul-pied usa o manifesto¶
- Reconciliação (
ReconciliacaoJob):CONFIRMADOsó quando não há remessa viva e existe ao menos umaENTREGUE.RESOLVIDA_MANUALnão é viva, mas também não conta como entregue — o ERP recusou e quem resolveu foi gente; confirmar por ela diria que o X-Adm aceitou o que rejeitou. Status que este app não conhece conta como viva (conservador: nunca viraCONFIRMADO). - Botão "Importado manualmente" (
PainelController, só em Falha): chama oresolver-manualantes de marcar o pedido aqui, e só o200(resolvidas1, ou 0 = nada vivo lá) levaERRO_XADM → IMPORTADO_MANUAL. O409(parte do pedido ainda pendente de entrega: remessaABERTA, ex. depois de um re-arme, ouTRAVADAcom filho000bloqueado por um pai1XX) e a falha de transporte deixam o pedido em Falha, com flash dizendo por quê — no409, não lançar à mão, que duplicaria: aguardar, ou corrigir o dado e Re-Importar (o detalhe volta com?pendente=1, que põe o botão à mostra). Na ordem inversa, uma falha do comando deixaria o pedido fechado aqui e a remessaTRAVADAlá para sempre, sem ninguém que a reconcilie de novo. operador: opcional na tela (o painel não tem login), lembrado no navegador; vai no corpo só quando preenchido. Localmente entra no fim da observação (importacao_manual_obs).- Fixtures do contrato: o
IntegradorClientTestlê deste arquivo os exemplosjson(listagem e resposta do resolver) e o de requisição do resolver (rota + corpo), e alimenta o servidor fake com eles (ContratoImportado). Atualizar o pin re-testa o parser contra o contrato novo.
O campo é itensRejeitados — não itens
O nome vem do RemessaResponse do dono. Este app lia itens e recebia sempre lista vazia: a
reconciliação gravava "Remessa travada no X-Adm." sem cod_retorno, e sem ele o alerta 1XX
não disparava e o reenvio não re-armava o espelho. Quinze pedidos-kit ficaram parados em silêncio
(2026-09-10). As fixtures de teste também usavam itens e por isso passavam — daí elas saírem,
agora, do exemplo do contrato.
A rota por tabela (GET /api/v1/xadm/retorno/{tabela}) não reconcilia — só alimenta o diagnóstico do painel
Desde 2026-09-10 o detalhe do pedido em Falha a consulta ao vivo (DiagnosticoService →
IntegradorClient.consultarRetorno): a remessa só lista o que foi rejeitado, e o operador
precisa ver o que o X-Adm gravou (e o código do contrato lá) para lançar o resto à mão. Na
reconciliação ela segue fora, pelo motivo abaixo.
Ela responde uma linha por chave natural e cobre só contratos/estoque/itensped/propriedades/
fones — não formulas. Reconciliar por ela deixava item e fórmula rejeitados invisíveis
daqui: o pedido 260079618 ficou CONFIRMADO com o itensped em 120, e 64 das 76 linhas
travadas eram fórmulas. Além disso itensped exige xPed e codigoAlt (sem os dois é
400), então nem dá para varrer um pedido por ela. Ver
decisão 0021.
Contrato do dono ainda não publicado — dívida do Integrador
Este endpoint é do Integrador (XadmRetornoController, criado justamente para a reconciliação
da PIED). Hoje o dono já publica o fato (raw/xadm-retorno.md, que cobre também o re-arme da
§6), mas este repo ainda não o importou — só o xadm-ingest, o xadm-ingest-estoque e o
integracao-remessa. Por isso aqui há link, não cópia: re-digitar o contrato de retorno
recriaria exatamente o apodrecimento que a §1 acabou de matar.
A dívida é do dono, não deste repo (constituição §2): o certo é um PR no
integracao/integrador publicando o fato do retorno, e então importá-lo aqui como os outros
dois. Enquanto isso, a fonte é o código do dono — XadmRetornoController — e o cliente deste
lado é o IntegradorClient.consultarRetornoContrato.
6. Re-arme do espelho — POST /api/v1/xadm/retorno/pedido/reenfileirar¶
Rota que o maxsul-pied chama no PushEnviador, logo depois da entrega confirmada
(IntegradorClient.reenfileirarPedido), e só para pedido que carrega rejeição terminal
(cod_retorno 1XX gravado).
Não é mais o clique do botão — e a razão é a ordem obrigatória abaixo
Até 2026-09 quem chamava era o botão Reimportar do painel. Mas o botão só enfileira: o
PUT sai até 2 min depois, pelo relay. Re-armar no clique reabria a linha no espelho antes
de o dado novo chegar — exatamente o que a ordem obrigatória proíbe. Movendo a chamada para
depois do marcarEnviado, a ordem passa a valer por construção, e vale igual para o caminho
automático (dado novo da PIED reenfileira sozinho — desde 2026-09-10 só o ERRO de envio; o
ERRO_XADM se resolve à mão, revisão da decisão 0006). Ver
decisão 0021.
Por que existe. O write-back (§4) grava cod_retorno nas linhas do espelho, e o cliente do X-Adm
só coleta WHERE cod_retorno = '000' OR LIKE '9%'. Linha em 1XX (rejeição de dado) é invisível
para ele para sempre. O Integrador não zera esse campo sozinho — é a regra de mão única: sem
ela, um re-push de rotina apagaria o retorno de um pedido já gravado e duplicaria pedido no ERP.
Esta rota é a exceção sob comando: quem pede é o operador, depois de corrigir o dado.
Ordem obrigatória: push (§1) primeiro, re-arme depois. São transações separadas — re-armar antes deixaria o cliente do ERP coletar a linha com o dado velho. Com esta ordem, quando a linha volta a ser coletável já está atualizada.
POST /api/v1/xadm/retorno/pedido/reenfileirar?xPed=<code>
Authorization: Bearer $INTEGRADOR_API_TOKEN
Resposta = contagem de linhas re-armadas por tabela; este lado usa o total:
{ "xPed": "260079421", "reArmadas": { "contratos": 1, "itensped": 3, "formulas": 2 }, "total": 6 }
Leitura tolerante de propósito (IntegradorClient.contarReArmadas): usa total quando vier, senão
soma os números do mapa (reArmadas, tabelas ou raiz). Corpo sem número nenhum é contrato
quebrado → erro, nunca "zero linhas" — o painel não pode dizer que reenviou sem ter reenviado.
Zero é resposta legítima (não havia linha em 1XX) e o painel a mostra como tal; falha de transporte
vira flash de erro explícito.
Mesma dívida da §5 — e um pino a mais
Este endpoint também é do Integrador e também não está publicado como fato no raw/ dele, daí
o contrato acima estar re-digitado (o que a §1 combate). Pior: ele nasceu depois deste cliente
— o formato do envelope foi acordado por handoff, não importado. Enquanto o dono não publicar, a
verdade é o código dele (XadmRetornoController) e a tolerância do contarReArmadas é o
para-choque. O certo segue sendo o PR no integracao/integrador publicando §5 e §6.